add rbac module

This commit is contained in:
liuyi 2025-06-30 13:12:21 +08:00
parent fab90132b0
commit 9461fc53f3
12 changed files with 286 additions and 78 deletions

View File

@ -18,6 +18,8 @@ import {
UpdateEvent,
} from 'typeorm';
import { LoadEvent } from 'typeorm/subscriber/event/LoadEvent';
import { Configure } from '@/modules/config/configure';
import { app } from '@/modules/core/helpers/app';
@ -72,7 +74,7 @@ export abstract class BaseSubscriber<T extends ObjectLiteral>
return this.entity;
}
async afterLoad(entity: any) {
async afterLoad(entity: any, event?: LoadEvent<T>) {
if ('parent' in entity && isNil(entity.depth)) {
entity.depth = 0;
}

View File

@ -1,2 +1 @@
export * from './permission.controller';
export * from './role.controller';

View File

@ -1 +1,2 @@
export * from './role.controller';
export * from './permission.controller';

View File

@ -0,0 +1 @@
export * from './user.controller';

View File

@ -0,0 +1,102 @@
import {
Body,
Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Patch,
Post,
Query,
SerializeOptions,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { DeleteWithTrashDto, RestoreDto } from '@/modules/content/dtos/delete.with.trash.dto';
import { PermissionAction } from '@/modules/rbac/constants';
import { Permission } from '@/modules/rbac/decorators/permission.decorator';
import { PermissionChecker } from '@/modules/rbac/types';
import { Depends } from '@/modules/restful/decorators/depend.decorator';
import { CreateUserDto, FrontedQueryUserDto, UpdateUserDto } from '@/modules/user/dtos/user.dto';
import { UserEntity } from '@/modules/user/entities';
import { UserService } from '@/modules/user/services';
import { UserModule } from '@/modules/user/user.module';
const permission: PermissionChecker = async (ab) =>
ab.can(PermissionAction.MANAGE, UserEntity.name);
@ApiTags('用户管理')
@Depends(UserModule)
@ApiBearerAuth()
@Controller('users')
export class UserController {
constructor(protected service: UserService) {}
/**
*
*/
@Get()
@Permission(permission)
@SerializeOptions({ groups: ['user-list'] })
async list(@Query() options: FrontedQueryUserDto) {
return this.service.list(options);
}
/**
*
* @param id
*/
@Get(':id')
@Permission(permission)
@SerializeOptions({ groups: ['user-detail'] })
async detail(@Param('id', new ParseUUIDPipe()) id: string) {
return this.service.detail(id);
}
/**
*
* @param data
*/
@Post()
@Permission(permission)
@SerializeOptions({ groups: ['user-detail'] })
async store(@Body() data: CreateUserDto) {
return this.service.create(data);
}
/**
*
* @param data
*/
@Patch()
@Permission(permission)
@SerializeOptions({ groups: ['user-detail'] })
async update(@Body() data: UpdateUserDto) {
return this.service.update(data);
}
/**
*
* @param data
*/
@Delete()
@Permission(permission)
@SerializeOptions({ groups: ['user-list'] })
async delete(@Body() data: DeleteWithTrashDto) {
const { ids, trash } = data;
return this.service.delete(ids, trash);
}
/**
*
* @param data
*/
@Patch('restore')
@Permission(permission)
@SerializeOptions({ groups: ['user-list'] })
async restore(@Body() data: RestoreDto) {
const { ids } = data;
return this.service.restore(ids);
}
}

View File

@ -1,26 +1,18 @@
import {
Body,
Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Patch,
Post,
SerializeOptions,
} from '@nestjs/common';
import { Controller, Get, Param, ParseUUIDPipe, Query, SerializeOptions } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { ApiTags } from '@nestjs/swagger';
import { DeleteWithTrashDto, RestoreDto } from '@/modules/content/dtos/delete.with.trash.dto';
import { IsNull, Not } from 'typeorm';
import { SelectTrashMode } from '@/modules/database/constants';
import { Depends } from '@/modules/restful/decorators/depend.decorator';
import { UserService } from '@/modules/user/services';
import { UserModule } from '@/modules/user/user.module';
import { Guest } from '../decorators/guest.decorator';
import { CreateUserDto, UpdateUserDto } from '../dtos/user.dto';
import { UserService } from '../services/user.service';
import { FrontedQueryUserDto } from '../dtos/user.dto';
@ApiTags('用户管理')
@ApiTags('用户查询')
@Depends(UserModule)
@Controller('users')
export class UserController {
@ -32,8 +24,8 @@ export class UserController {
@Get()
@Guest()
@SerializeOptions({ groups: ['user-list'] })
async list() {
return this.service.list();
async list(@Query() options: FrontedQueryUserDto) {
return this.service.list({ ...options, trashed: SelectTrashMode.NONE });
}
/**
@ -44,52 +36,6 @@ export class UserController {
@Guest()
@SerializeOptions({ groups: ['user-detail'] })
async detail(@Param('id', new ParseUUIDPipe()) id: string) {
return this.service.detail(id);
}
/**
*
* @param data
*/
@Post()
@ApiBearerAuth()
@SerializeOptions({ groups: ['user-detail'] })
async store(@Body() data: CreateUserDto) {
return this.service.create(data);
}
/**
*
* @param data
*/
@Patch()
@ApiBearerAuth()
@SerializeOptions({ groups: ['user-detail'] })
async update(@Body() data: UpdateUserDto) {
return this.service.update(data);
}
/**
*
* @param data
*/
@Delete()
@ApiBearerAuth()
@SerializeOptions({ groups: ['user-list'] })
async delete(@Body() data: DeleteWithTrashDto) {
const { ids, trash } = data;
return this.service.delete(ids, trash);
}
/**
*
* @param data
*/
@Patch('restore')
@ApiBearerAuth()
@SerializeOptions({ groups: ['user-list'] })
async restore(@Body() data: RestoreDto) {
const { ids } = data;
return this.service.restore(ids);
return this.service.detail(id, async (qb) => qb.andWhere({ deletedAt: Not(IsNull()) }));
}
}

View File

@ -1,8 +1,10 @@
import { PartialType, PickType } from '@nestjs/swagger';
import { OmitType, PartialType, PickType } from '@nestjs/swagger';
import { IsDefined, IsEnum, IsUUID } from 'class-validator';
import { IsDefined, IsEnum, IsOptional, IsUUID } from 'class-validator';
import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator';
import { IsDataExist } from '@/modules/database/constraints';
import { PermissionEntity, RoleEntity } from '@/modules/rbac/entities';
import { PaginateWithTrashedDto } from '@/modules/restful/dtos/paginate-width-trashed.dto';
import { UserOrderType, UserValidateGroup } from '@/modules/user/constants';
import { UserCommonDto } from '@/modules/user/dtos/user.common.dto';
@ -17,7 +19,39 @@ export class CreateUserDto extends PickType(UserCommonDto, [
'email',
'password',
'phone',
]) {}
]) {
/**
* ID列表
*/
@IsDataExist(RoleEntity, {
each: true,
always: true,
message: '角色不存在',
})
@IsUUID(undefined, {
each: true,
always: true,
message: '角色ID格式不正确',
})
@IsOptional({ always: true })
roles?: string[];
/**
* ID列表
*/
@IsDataExist(PermissionEntity, {
each: true,
always: true,
message: '权限不存在',
})
@IsUUID(undefined, {
each: true,
always: true,
message: '权限ID格式不正确',
})
@IsOptional({ always: true })
permissions?: string[];
}
/**
*
@ -36,9 +70,35 @@ export class UpdateUserDto extends PartialType(CreateUserDto) {
* Query数据验证
*/
export class QueryUserDto extends PaginateWithTrashedDto {
/**
* 角色ID:根据角色来过滤用户
*/
@IsDataExist(RoleEntity, {
message: '角色不存在',
})
@IsUUID(undefined, { message: '角色ID格式错误' })
@IsOptional()
role?: string;
/**
* 权限ID:根据权限来过滤用户()
*/
@IsDataExist(PermissionEntity, {
message: '权限不存在',
})
@IsUUID(undefined, { message: '权限ID格式错误' })
@IsOptional()
permission?: string;
/**
* 排序规则:可指定用户列表的排序规则,
*/
@IsEnum(UserOrderType)
orderBy?: UserOrderType;
}
/**
*
*/
@DtoValidation({ type: 'query' })
export class FrontedQueryUserDto extends OmitType(QueryUserDto, ['trashed']) {}

View File

@ -7,6 +7,9 @@ export class UserRepository extends BaseRepository<UserEntity> {
protected _qbName: string = 'user';
buildBaseQuery() {
return this.createQueryBuilder(this.qbName).orderBy(`${this.qbName}.createdAt`, 'DESC');
return this.createQueryBuilder(this.qbName)
.orderBy(`${this.qbName}.createdAt`, 'DESC')
.leftJoinAndSelect(`${this.qbName}.roles`, 'roles')
.leftJoinAndSelect(`${this.qbName}.permissions`, 'permissions');
}
}

View File

@ -1,9 +1,10 @@
import { RouteOption, TagOption } from '../restful/types';
import * as controllers from './controllers';
import * as managerControllers from './controllers/manager';
export function createUserApi() {
const routes: Record<'app', RouteOption[]> = {
const routes: Record<'app' | 'manager', RouteOption[]> = {
app: [
{
name: 'app.user',
@ -11,13 +12,21 @@ export function createUserApi() {
controllers: Object.values(controllers),
},
],
manager: [
{
name: 'app.user',
path: 'manager',
controllers: Object.values(managerControllers),
},
],
};
const tags: Record<'app', (string | TagOption)[]> = {
const tags: Record<'app' | 'manager', (string | TagOption)[]> = {
app: [
{ name: '用户管理', description: '对用户进行CRUD操作' },
{ name: '账户操作', description: '注册登录、查看修改账户信息、修改密码等' },
],
manager: [{ name: '用户管理', description: '管理用户信息' }],
};
return { routes, tags };

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { isNil } from 'lodash';
import { isArray, isNil } from 'lodash';
import { DataSource, EntityNotFoundError, SelectQueryBuilder } from 'typeorm';
import { Configure } from '@/modules/config/configure';
@ -8,6 +8,8 @@ import { BaseService } from '@/modules/database/base/service';
import { QueryHook } from '@/modules/database/types';
import { SystemRoles } from '@/modules/rbac/constants';
import { RoleRepository } from '@/modules/rbac/repositories';
import { UserRepository } from '@/modules/user/repositories';
import { CreateUserDto, QueryUserDto, UpdateUserDto } from '../dtos/user.dto';
@ -21,26 +23,68 @@ export class UserService extends BaseService<UserEntity, UserRepository> {
protected configure: Configure,
protected dataSource: DataSource,
protected userRepository: UserRepository,
protected roleRepository: RoleRepository,
) {
super(userRepository);
}
/**
*
* @param roles
* @param permissions
* @param data
*/
async create(data: CreateUserDto): Promise<UserEntity> {
async create({ roles, permissions, ...data }: CreateUserDto): Promise<UserEntity> {
const user = await this.userRepository.save(data, { reload: true });
if (isArray(roles) && roles.length > 0) {
await this.userRepository
.createQueryBuilder('user')
.relation('roles')
.of(user)
.add(roles);
}
if (isArray(permissions) && permissions.length > 0) {
await this.userRepository
.createQueryBuilder('user')
.relation('permissions')
.of(user)
.add(permissions);
}
await this.addUserRole(await this.detail(user.id));
return this.detail(user.id);
}
/**
*
* @param roles
* @param permissions
* @param data
*/
async update(data: UpdateUserDto): Promise<UserEntity> {
async update({ roles, permissions, ...data }: UpdateUserDto): Promise<UserEntity> {
const updated = await this.userRepository.save(data, { reload: true });
return this.detail(updated.id);
const user = await this.detail(updated.id);
if (
(isNil(roles) || roles.length <= 0) &&
(isNil(permissions) || permissions.length <= 0)
) {
return user;
}
if (isArray(roles) && roles.length > 0) {
await this.userRepository
.createQueryBuilder('user')
.relation('roles')
.of(user)
.addAndRemove(roles, user.roles ?? []);
}
if (isArray(permissions) && permissions.length > 0) {
await this.userRepository
.createQueryBuilder('user')
.relation('permissions')
.of(user)
.addAndRemove(permissions, user.permissions ?? []);
}
await this.addUserRole(await this.detail(user.id));
return this.detail(user.id);
}
/**
@ -87,9 +131,32 @@ export class UserService extends BaseService<UserEntity, UserRepository> {
) {
const { orderBy } = options;
const qb = await super.buildListQB(queryBuilder, options, callback);
if (!isNil(options.role)) {
qb.andWhere('roles.id IN (:...roles', { roles: [options.role] });
}
if (!isNil(options.permission)) {
qb.andWhere('permissions.id IN (:...permissions', {
permissions: [options.permission],
});
}
if (!isNil(orderBy)) {
qb.orderBy(`${this.repository.qbName}.${orderBy}`, 'ASC');
}
return qb;
}
protected async addUserRole(user: UserEntity) {
const roleRelation = this.userRepository.createQueryBuilder().relation('roles').of(user);
const roleNames = (user.roles ?? []).map((role) => role.name);
const noneUserRole = roleNames.length <= 0 || !roleNames.includes(SystemRoles.USER);
if (noneUserRole) {
const userRole = await this.roleRepository.findOne({
relations: ['users'],
where: { name: SystemRoles.USER },
});
if (!isNil(userRole)) {
await roleRelation.add(userRole);
}
}
}
}

View File

@ -1,8 +1,9 @@
import { randomBytes } from 'node:crypto';
import { EventSubscriber, InsertEvent, UpdateEvent } from 'typeorm';
import { EventSubscriber, InsertEvent, LoadEvent, UpdateEvent } from 'typeorm';
import { BaseSubscriber } from '@/modules/database/base/subscriber';
import { RoleEntity } from '@/modules/rbac/entities';
import { UserEntity } from '@/modules/user/entities/user.entity';
import { encrypt } from '@/modules/user/utils';
@ -36,4 +37,21 @@ export class UserSubscriber extends BaseSubscriber<UserEntity> {
event.entity.password = await encrypt(this.configure, event.entity.password);
}
}
async afterLoad(user: UserEntity, event: LoadEvent<any>): Promise<void> {
let permissions = user.permissions ?? [];
for (const role of user.roles ?? []) {
const roleEntity = await this.getManage(event).findOneOrFail(RoleEntity, {
relations: ['permissions'],
where: { id: role.id },
});
permissions = [...permissions, ...(roleEntity.permissions ?? [])];
}
user.permissions = permissions.reduce((o, n) => {
if (o.find(({ name }) => name === n.name)) {
return o;
}
return [...o, n];
}, []);
}
}