Skip to main content

模型动态关联与基于CASL的动态权限的实现

学习目标

  • 实现模型间的动态关联
  • 掌握TypeORM事务构建
  • 深入了解Nestjs模块的生命周期以及ModuleRef的灵活运用
  • 编写一个用于存储动态权限与角色的RBAC模块
  • 实现websocket和Http的权限验证守卫
  • 实现CRUD框架的RBAC装饰器
  • 实现基于CASL的动态权限验证
  • 分离应用的前后台API

流程图

未命名文件(5)

预装类库

在开始编码之前请安装以下类库

~ pnpm add @casl/ability

代码结构

这一节课我们添加了一个RBAC模块用于验证权限

src/modules/rbac
├── constants.ts
├── controllers
│   ├── index.ts
│   ├── permission.controller.ts
│   └── role.controller.ts
├── decorators
│   ├── has-permission.decorator.ts # 用于添加API权限验证的装饰器
│   ├── index.ts
│   └── rbac-crud.decorator.ts # CRUD装饰器的RBAC实现
├── dtos
│   ├── index.ts
│   ├── permission.dto.ts
│   └── role.dto.ts
├── entities
│   ├── index.ts
│   ├── permission.entity.ts
│   └── role.entity.ts
├── guards
│   ├── checker.ts
│   ├── index.ts
│   ├── rbac-ws.guard.ts # 适用于websocket的权限守卫
│   └── rbac.guard.ts # 适用于http的权限守卫
├── helpers.ts
├── rbac.module.ts
├── repositories
│   ├── index.ts
│   ├── permission.repository.ts
│   └── role.repository.ts
├── resolver
│   ├── index.ts
│   └── rbac.resolver.ts # 用于同步权限和系统角色的提供者
├── services
│   ├── index.ts
│   ├── permission.service.ts
│   └── role.service.ts
├── subscribers
│   ├── index.ts
│   ├── permission.subscriber.ts
│   └── role.subscriber.ts
└── types.ts

动态关联

比较常用的需要动态关联的场景是用户模型,我们为用户模型添加一个动态关联

首先添加一个用于定义动态关联的类型

// src/modules/core/types.ts
export interface DynamicRelation {
relation:
| ReturnType<typeof OneToOne>
| ReturnType<typeof OneToMany>
| ReturnType<typeof ManyToOne>
| ReturnType<typeof ManyToMany>;
column: string;
}

添加一个动态关联装饰器,用于存储关联关系

// src/modules/core/constants.ts
export const ADDTIONAL_RELATIONS = 'additional_relations';

// src/modules/core/decorators/dynamic-relation.decorator.ts
export function AddRelations(relations: () => Array<DynamicRelation>) {
return <E extends ObjectLiteral>(target: E) => {
Reflect.defineMetadata(ADDTIONAL_RELATIONS, relations, target);
return target;
};
}

动态关联实现逻辑为读取ADDTIONAL_RELATIONS常量,通过该常量存储的值来添加关联的column字段与关联关系,最后把修改后的类通过TypeOrmModule.forFeature加载

export const loadEntities = (
entities: EntityClassOrSchema[] = [],
dataSource?: DataSource | DataSourceOptions | string,
) => {
...
return TypeOrmModule.forFeature(es, dataSource);
};

使用

配置用户模型的动态关联

// src/modules/user/types.ts

/**
* 自定义用户模块配置
*/
export interface UserConfig {
super: {
username: string;
password: string;
};
...
}

/**
* 默认用户模块配置
*/
export interface DefaultUserConfig {
super: {
username: string;
password: string;
};
...
}

// src/config/user.config.ts

/**
* 用户模块配置
*/
export const userConfig: () => UserConfig = () => ({
...
relations: [
{
column: 'posts',
relation:
},
{
column: 'comment',
relation:
},
],
});

为模型添加装饰器并传入关联配置

// src/modules/user/entities/user.entity.ts
@AddRelations(() => getUserConfig<DynamicRelation[]>('relations'))
@Exclude()
@Entity('users')
export class UserEntity extends BaseEntity {
...
}

替换模型注册方式

@Module({
imports: [
loadEntities(entities),
...
})
export class UserModule {}

RBAC模块

RBAC模块基于CASL实现权限验证并通过数据库存储来构建动态权限

模型

添加两个模型PermissionEntityRoleEntity,分别用于存储权限和角色数据

权限,用户,角色三者的关系均为多对多

需要注意的是RoleEntity中如果把systemed设置成就几位无法删除而是自动同步到数据表的系统角色

// src/modules/rbac/entities/permission.entity.ts
export class PermissionEntity<
A extends AbilityTuple = AbilityTuple,
C extends MongoQuery = MongoQuery,
> {
...

@Expose({ groups: ['permission-list', 'permission-detail'] })
@ManyToMany((type) => RoleEntity, (role) => role.permissions)
@JoinTable()
roles!: RoleEntity[];

@ManyToMany(() => UserEntity, (user) => user.permissions)
@JoinTable()
users!: UserEntity[];
}

// src/modules/rbac/entities/role.entity.ts
@Exclude()
@Entity('rbac_roles')
export class RoleEntity extends BaseEntity {
...
@Column({ comment: '是否为不可更改的系统权限', default: false })
systemed?: boolean;

@Expose({ groups: ['role-detail'] })
@Type(() => PermissionEntity)
@ManyToMany(() => PermissionEntity, (permission) => permission.roles, {
cascade: true,
eager: true,
})
permissions!: PermissionEntity[];

@ManyToMany(() => UserEntity, (user) => user.roles, { deferrable: 'INITIALLY IMMEDIATE' })
@JoinTable()
users!: UserEntity[];
}

存储类

存储类继承我们前面编写的BaseRepository,两个存储类的作用在于在查询时添加角色关联的权限,以及添加权限关联的角色

// src/modules/rbac/repositories/permission.repository.ts
@CustomRepository(PermissionEntity)
export class PermissionRepository extends BaseRepository<PermissionEntity> {
protected qbName = 'permission';

buildBaseQuery() {
return this.createQueryBuilder(this.getQBName()).leftJoinAndSelect(
`${this.getQBName()}.roles`,
'roles',
);
}
}

// src/modules/rbac/repositories/role.repository.ts
@CustomRepository(RoleEntity)
export class RoleRepository extends BaseRepository<RoleEntity> {
protected qbName = 'role';

buildBaseQuery() {
return this.createQueryBuilder(this.getQBName()).leftJoinAndSelect(
`${this.getQBName()}.permissions`,
'permssions',
);
}
}

订阅者

两个订阅者的作用在于在查询时为没有设置label字段的权限和角色,把它们的name设置成label

// src/modules/rbac/subscribers/permission.subscriber.ts
@EventSubscriber()
export class PermssionSubscriber extends BaseSubscriber<PermissionEntity> {
...
async afterLoad(entity: PermissionEntity) {
if (isNil(entity.label)) {
entity.label = entity.name;
}
}
}

@EventSubscriber()
export class RoleSubscriber extends BaseSubscriber<RoleEntity> {
...

async afterLoad(entity: RoleEntity) {
if (isNil(entity.label)) {
entity.label = entity.name;
}
}
}

数据验证

对于权限,因为是固定不变的,只有在启动时同步一下到数据库,所以只需要查询的API即可

权限可根据其关联的角色进行过滤

export class QueryPermssionDto implements PaginateDto, TrashedDto {

@IsModelExist(RoleEntity, {
groups: ['update'],
message: '指定的角色不存在',
})
@IsUUID(undefined, { message: '角色ID格式错误' })
@IsOptional()
role?: string;
...
}

角色需要支持CRUD操作(系统角色只能读取,而不可进行其它操作)

角色可根据其关联的用户过滤

export class QueryRoleDto implements PaginateDto, TrashedDto {
@IsModelExist(UserEntity, {
groups: ['update'],
message: '指定的用户不存在',
})
@IsUUID(undefined, { message: '用户ID格式错误' })
@IsOptional()
user?: string;
...
}

@Injectable()
@DtoValidation({ groups: ['create'] })
export class CreateRoleDto {
...

@IsModelExist(PermissionEntity, {
each: true,
always: true,
message: '权限不存在',
})
@IsUUID(undefined, {
each: true,
always: true,
message: '权限ID格式不正确',
})
@IsOptional({ always: true })
permissions?: string[];
}

@Injectable()
@DtoValidation({ groups: ['update'] })
export class UpdateRoleDto extends PartialType(CreateRoleDto) {
...
}

服务

权限服务继承课程前面编写的BaseService,因为没有CRUD操作,所以只要重载一下查询方法,在查询列表时可根据角色过滤即可

@Injectable()
export class PermissionService extends BaseService<PermissionEntity, PermissionRepository> {
constructor(protected permissionRepository: PermissionRepository) {
super(permissionRepository);
}

protected async buildListQuery(
queryBuilder: SelectQueryBuilder<PermissionEntity>,
options: QueryPermssionDto,
callback?: QueryHook<PermissionEntity>,
) {
const qb = await super.buildListQuery(queryBuilder, options, callback);
if (!isNil(options.role)) {
qb.andWhere('roles.id IN (:...roles)', {
roles: [options.role],
});
}
return qb;
}
}

角色服务同样继承BaseService,支持CRUD操作

注意删除权限时判断一下是否systemed,如果是的话就抛出异常

@Injectable()
export class RoleService extends BaseService<RoleEntity, RoleRepository> {
protected enable_trash = true;

constructor(
protected roleRepository: RoleRepository,
protected permissionRepository: PermissionRepository,
) {
super(roleRepository);
}

async create(data: CreateRoleDto) {
...
}

async update(data: UpdateRoleDto) {
...
}

/**
* 删除数据
* @param id
* @param trash
*/
async delete(id: string, trash = true) {
const item = await this.repository.findOneOrFail({
where: { id } as any,
withDeleted: this.enable_trash ? true : undefined,
});
if (item.systemed) {
throw new ForbiddenException('can not remove systemed role!');
}
if (this.enable_trash && trash && isNil(item.deletedAt)) {
// await this.repository.softRemove(item);
(item as any).deletedAt = new Date();
await this.repository.save(item);
return this.detail(id, true);
}
return this.repository.remove(item);
}

protected async buildListQuery(
queryBuilder: SelectQueryBuilder<RoleEntity>,
options: QueryRoleDto,
callback?: QueryHook<RoleEntity>,
) {
const qb = await super.buildListQuery(queryBuilder, options, callback);
qb.leftJoinAndSelect(`${this.repository.getQBName()}.users`, 'users');
if (!isNil(options.user)) {
qb.andWhere('users.id IN (:...users)', {
roles: [options.user],
});
}
return qb;
}
}

同步数据

同步数据的操作需要我们先掌握以下两个概念

  • Nestjs的应用启动生命周期
  • TypeORM的事务操作

编写一个RbacResolver提供者,并实现OnApplicationBootstrap

  • optionssetOptions用于设置CASL的选项
  • _rolesaddRoles用于同步系统角色
  • _permissionsaddPermissions用于同步权限

默认自带两个角色,分别为普通用户与超级管理员

@Injectable()
export class RbacResolver<A extends AbilityTuple = AbilityTuple, C extends MongoQuery = MongoQuery>
implements OnApplicationBootstrap
{
protected setuped = false;

protected options: AbilityOptions<A, C>;

protected _roles: Role[] = [
{
name: SystemRoles.USER,
label: '普通用户',
description: '新用户的默认角色',
permissions: [],
},
{
name: SystemRoles.ADMIN,
label: '超级管理员',
description: '拥有整个系统的管理权限',
permissions: [],
},
];

protected _permissions: Permission<A, C>[] = [
{
name: 'system-manage',
label: '系统管理',
description: '管理系统的所有功能',
rule: {
action: 'manage',
subject: 'all',
} as any,
},
];

constructor(protected dataSource: DataSource) {}

setOptions(options: AbilityOptions<A, C>) {
if (!this.setuped) {
this.options = options;
this.setuped = true;
}
return this;
}

get roles() {
return this._roles;
}

get permissions() {
return this._permissions;
}

addRoles(data: Role[]) {
this._roles = [...this.roles, ...data];
}

addPermissions(data: Permission<A, C>[]) {
this._permissions = [...this.permissions, ...data].map((p) => {
if (typeof p.rule.subject === 'string') return p;
if ('modelName' in p.rule.subject) {
const { modelName } = p.rule.subject;
return { ...p, rule: { ...p.rule, subject: modelName } };
}
return { ...p, rule: { ...p.rule, subject: (p.rule.subject as any).name } };
});
}
...
}

构建一个用于同步权限和角色的事务

具体实现请查看源代码

   async onApplicationBootstrap() {
const queryRunner = this.dataSource.createQueryRunner();

await queryRunner.connect();
await queryRunner.startTransaction();

try {
await this.syncRoles(queryRunner.manager);
await this.syncPermissions(queryRunner.manager);
await queryRunner.commitTransaction();
} catch (err) {
console.log(err);
await queryRunner.rollbackTransaction();
} finally {
await queryRunner.release();
}
}
protected async syncRoles(manager: EntityManager) {
...
}

protected async syncPermissions(manager: EntityManager) {
...
}

有一个比较重要的规则是,权限虽然放入数据库,但是不会把判断条件的conditions属性存储,因为它是一个函数,此属性存储在_permissions中,在读取权限时根据名称获取此属性用于作为casl判断的条件

除了通过RbacResolver同步的系统角色外,其它角色我们可以灵活添加与移除,而权限总是固定的,即时移除,在重启或下一次启动应用时又会自动同步。

那么这些角色和权限在哪里定义,什么时候定义呢?

这几需要了解nestjs的生命周期了,可以看到下图

lifecycle-events

在启动应用时,对于提供者,首先我们会去判断是否有onModuleinit方法,有的话就执行,然后会去判断是否有onApplicationBootstrap方法,有的话就执行,利用这个机制就很容易构建出我们的权限同步功能

首先我们需要在各个模块增加一个用于添加角色和权限的提供者,并且在onMoudleinit时期使用RbacResolver添加好权限与角色,并在RbacResolveronApplicationBootstrap时期同步权限与角色即可

ContentRbac为例

使用addRoles方法不仅仅是添加角色,并且也会对已存在或已经在其它模块添加的角色进行更新

@Injectable()
export class ContentRbac implements OnModuleInit {
constructor(private moduleRef: ModuleRef) {}

onModuleInit() {
const resolver = this.moduleRef.get(RbacResolver, { strict: false });
resolver.addPermissions([
{
name: 'post.create',
rule: {
action: PermissionAction.CREATE,
subject: PostEntity,
},
},
{
name: 'post.owner',
rule: {
action: PermissionAction.OWNER,
subject: PostEntity,
conditions: (user) => ({
'author.id': user.id,
}),
},
},
...
]);
resolver.addRoles([
{
name: SystemRoles.USER,
permissions: [
'post.read',
'post.create',
'post.owner',
'comment.create',
'comment.owner',
],
},
{
name: 'content-manage',
label: '内容管理员',
description: '管理内容模块',
permissions: ['post.manage', 'category.manage', 'comment.manage'],
},
]);
}
}

然后我们把这些模块的rbac.ts提供者在本模块注册,把RbacResolverRbac模块注册,使它们成为提供者即可。需要注意的是,要使用Rbac功能的模块需要导入RbacModule

因为UserModuleRbacModule是相互循环依赖,所以需要用forWord相互导入

// src/modules/user/user.module.ts

@Module({
imports: [
loadEntities(entities),
...
forwardRef(() => RbacModule),
],
})
export class UserModule {}

// src/modules/rbac/rbac.module.ts

@Module({
imports: [
forwardRef(() => UserModule),
TypeOrmModule.forFeature(entities),
CoreModule.forRepository(repositories),
],
controllers,
providers: [
...
{
provide: RbacResolver,
useFactory: async (dataSource: DataSource) => {
const resolver = new RbacResolver(dataSource);
resolver.setOptions({});
return resolver;
},
inject: [getDataSourceToken()],
},
],
exports: [CoreModule.forRepository(repositories), RbacResolver],
})
export class RbacModule {}

守卫与装饰器

在添加守卫之前先增加以下两个函数

文件位置: src/modules/rbac/guards/rbac.guard.ts

它们分别用于

  • getCheckers用于通过PERMISSION_CHECKERS的装饰器元数据获取控制器方法上的权限验证器
  • exeChecker用于执行控制器方法上的权限验证器函数或类
  • solveChecker根据当前登录用户关联的权限调用exeChcker用于验证控制器方法上的所有验证器,并返回最终结果

当前用户的权限是根据其关联的角色下的所有权限以及其直接关联的权限合并去重后所得,这需要为用户模型的订阅者添加如下代码

// src/modules/user/subscribers/user.subscriber.ts
async afterLoad(entity: UserEntity): Promise<void> {
let permissions = (entity.permissions ?? []) as PermissionEntity[];
for (const role of entity.roles ?? []) {
const roleEntity = await RoleEntity.findOneOrFail({
relations: ['permissions'],
where: { id: role.id },
});
permissions = [...permissions, ...(roleEntity.permissions ?? [])];
}
entity.permissions = permissions.reduce((o, n) => {
if (o.find(({ name }) => name === n.name)) return o;
return [...o, n];
}, []);
}

然后添加一个用于定义PERMISSION_CHECKERS(权限验证器列表)的装饰器

// src/modules/rbac/decorators/has-permission.decorator.ts
export const Permission = (...checkers: PermissionChecker[]) =>
SetMetadata(PERMISSION_CHECKERS, checkers);

同时,为了方便给Crud装饰器装饰的控制器方法添加权限验证器,需要定义一个RbacCrud装饰器,此装饰器首先执行Crud,然后为原来的CurdOptions添加一个rbac选项,用于配置权限验证器列表

// src/modules/rbac/types.ts

export type RbacCurdOption = CrudMethodOption & { rbac?: PermissionChecker[] };
export interface RbacCurdItem {
name: CurdMethod;
option?: RbacCurdOption;
}
export type RbacCurdOptions = Omit<CurdOptions, 'enabled'> & {
enabled: Array<CurdMethod | RbacCurdItem>;
};

// src/modules/rbac/decorators/rbac-crud.decorator.ts
export const RbacCrud =
(options: RbacCurdOptions) =>
<T extends BaseController<any>>(Target: Type<T>) => {
Crud(options)(Target);
const { enabled } = Reflect.getMetadata(CRUD_OPTIONS, Target) as RbacCurdOptions;
// 添加验证DTO类
for (const value of enabled) {
const { name } = (typeof value === 'string' ? { name: value } : value) as RbacCurdItem;
const find = enabled.find((v) => v === name || (v as any).name === name);
const option = typeof find === 'string' ? {} : find.option ?? {};
if (option.rbac) {
Reflect.defineMetadata(PERMISSION_CHECKERS, option.rbac, Target.prototype, name);
}
}
return Target;
};

添加一个继承自JwtAuthGuard的守卫,用于在登录守卫之后验证权限

// src/modules/rbac/guards/rbac.guard.ts

@Injectable()
export class RbacGuard extends JwtAuthGuard {
constructor(
protected reflector: Reflector,
protected resolver: RbacResolver,
protected tokenService: TokenService,
protected userRepository: UserRepository,
protected moduleRef: ModuleRef,
) {
super(reflector, tokenService);
}

async canActivate(context: ExecutionContext): Promise<boolean> {
const authCheck = await super.canActivate(context);
...
return solveChecker({
resolver: this.resolver,
checkers,
moduleRef: this.moduleRef,
user,
request,
});
}
}

并且把它替换掉JwtAuthGuard来作为全局守卫

// src/modules/user/user.module.ts
@Module({
// {
// provide: APP_GUARD,
// useClass: JwtAuthGuard,
// },
...
})

// src/modules/rbac/rbac.module.ts
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RbacGuard,
},
...
],
exports: [CoreModule.forRepository(repositories), RbacResolver],
})
export class RbacModule {}

接下来同样的创建一个继承JwtWsGuardRbacWsGuard用于对websockets发送消息时进行权限认证,必须是至少拥有普通用户角色才拥有创建消息的权限

当用户被设置为禁用(即actived字段是false)时,他的普通用户角色将被取消关联并且所有关联的权限将被清空

// src/modules/user/guards/jwt-ws.guard.ts

@Injectable()
export class JwtWsGuard implements CanActivate {
constructor(protected tokenService: TokenService) {}

/**
* 守卫方法
* @param context
*/
async canActivate(context: ExecutionContext) {
const { token } = context.switchToWs().getData();
if (!token) {
throw new WsException('Missing access token');
}
// 判断token是否存在,如果不存在则认证失败
const accessToken = await this.tokenService.checkAccessToken(token);
if (!accessToken) throw new WsException('Access token incorrect');
const user = await this.tokenService.verifyAccessToken(accessToken);
return !isNil(user);
}
}

辅助函数

为了更加简洁地添加Owner验证,编写一个两个辅助函数来封装

它们的作用如下

  • getRequestItems: 获取请求中的items,item,id,用于crud操作时验证数据
  • checkOwner: 验证是否是数据拥有者
export const getRequestItems = (request?: Request): string[] => {
const { params = {}, body = {} } = (request ?? {}) as any;
const id = params.id ?? body.id ?? params.item ?? body.item;
const { items } = body;
if (!isNil(id)) return [id];
return !isNil(items) && isArray(items) ? items : [];
};

export const checkOwner = async <E extends ObjectLiteral>(
ability: MongoAbility,
getModels: (items: string[]) => Promise<E[]>,
request?: Request,
permission?: string,
) => {
const models = await getModels(getRequestItems(request));
return models.every((model) => ability.can(permission ?? PermissionAction.OWNER, model));
};

验证权限

首先我们需要对应用的控制器和DTO做一个比较大的调整,目的是为了分离前台和后台的API,文件结构如下

RbacModule(只有后台操作)和UserModule(编写时已经前后台操作分离)无需调整,主要调整在ContentModule

src/modules
├── content
│ ├── controllers
│ │ ├── category.controller.ts
│ │ ├── comment.controller.ts
│ │ ├── index.ts
│ │ ├── manage
│ │ └── post.controller.ts
│ ├── dtos
│ │ ├── category.dto.ts
│ │ ├── comment.dto.ts
│ │ ├── index.ts
│ │ ├── manage
│ │ └── post.dto.ts
├── rbac
│ ├── controllers
│ │ ├── index.ts
│ │ ├── permission.controller.ts
│ │ └── role.controller.ts
│ ├── dtos
│ │ ├── index.ts
│ │ ├── permission.dto.ts
│ │ └── role.dto.ts
└── user
├── controllers
│ ├── account.controller.ts
│ ├── auth.controller.ts
│ ├── captcha.controller.ts
│ ├── index.ts
│ ├── manage
│ └── message.controller.ts
├── dtos
│ ├── account.dto.ts
│ ├── auth.dto.ts
│ ├── captcha.dto.ts
│ ├── guest.dto.ts
│ ├── index.ts
│ ├── message.dto.ts
│ └── user.dto.ts

ContentModule的文章和文章管理控制器为例,我们为前台操作添加createowner的权限验证,为后台操作添加contentmanage权限验证

需要注意的是all模式对所有模型的验证都适配,manage也同样是一个casl的关键字,对所有的操作(如create)等都适配,也就是系统管理权限可以做任意操作

// 创建文章权限验证
const createChecker: PermissionChecker = async (ab) =>
ab.can(PermissionAction.CREATE, PostEntity.name);

// 文章拥有者验证
const ownerChecker: PermissionChecker = async (ab, ref, request) =>
checkOwner(
ab,
async (items) =>
ref.get(PostRepository, { strict: false }).find({
relations: ['author'],
where: { id: In(items) },
}),
request,
);

const option: RbacCurdOption = {
rbac: [ownerChecker],
};

/**
* 文章控制器
*/
@RbacCrud({
id: 'post',
enabled: [
{ name: 'list', option: { allowGuest: true } },
{ name: 'detail', option: { allowGuest: true } },
{ name: 'store', option: { rbac: [createChecker] } },
{ name: 'update', option },
{ name: 'delete', option },
{ name: 'deleteMulti', option },
],
dtos: {
query: QueryPostDto,
create: CreatePostDto,
update: UpdatePostDto,
},
})
@Controller('content/posts')
export class PostController extends BaseController<PostService> {
constructor(protected postService: PostService) {
super(postService);
}

@Post()
async store(
@Body() data: CreatePostDto,
@ReqUser() user: ClassToPlain<UserEntity>,
): Promise<PostEntity> {
return this.service.create({ ...data, author: user.id });
}

@Patch()
async update(@Body() data: UpdatePostDto) {
return this.postService.update(omit(data, 'author'));
}
...
}

const queryPublished = (isPublished?: boolean) => {
if (typeof isPublished === 'boolean') {
return isPublished ? { publishedAt: Not(IsNull()) } : { publishedAt: IsNull() };
}
return {};
};

/**
* 在查询列表时,只有自己才能查看自己未发布的文章
* @param options
* @param author
*/
const queryListCallback: (
options: QueryPostDto,
author: ClassToPlain<UserEntity>,
) => QueryHook<PostEntity> = (options, author) => async (qb) => {
...
};

/**
* 在查询文章详情时,只有自己才能查看自己未发布的文章
* @param author
*/
const queryItemCallback: (author: ClassToPlain<UserEntity>) => QueryHook<PostEntity> =
(author) => async (qb) => {
...
};

而对于后台管理验证则要简单的多

// src/modules/content/controllers/manage/post.controller.ts

// 文章管理权限验证
const option: RbacCurdOption = {
rbac: [async (ab) => ab.can(PermissionAction.MANAGE, PostEntity.name)],
};
/**
* 文章控制器
*/
@RbacCrud({
id: 'post',
enabled: [
{ name: 'list', option },
{ name: 'detail', option },
'store',
{ name: 'update', option },
{ name: 'delete', option },
{ name: 'restore', option },
{ name: 'deleteMulti', option },
{ name: 'restoreMulti', option },
],
dtos: {
query: QueryPostDto,
create: ManageCreatePostDto,
update: ManageUpdatePostDto,
},
})
@Controller('manage/content/posts')
export class PostManageController extends BaseController<PostService> {
constructor(protected postService: PostService) {
super(postService);
}

@Post()
@Permission(option.rbac[0])
async store(
@Body() data: ManageCreatePostDto,
@ReqUser() user: ClassToPlain<UserEntity>,
): Promise<PostEntity> {
const author = isNil(data.author)
? user
: ({ id: data.author } as ClassToPlain<UserEntity>);
return this.service.create({ ...data, author: author.id });
}
}

同样的我们可以使用Permission装饰器来添加验证器,如

// src/modules/content/controllers/comment.controller.ts
@Delete(':id')
@Permission(checkers.owner)
async delete(
@Param('id', new ParseUUIDPipe())
id: string,
) {
return this.commentService.delete(id);
}

最后我们可以通过Thunder Client测试一下所有更改之后的接口

在测试前记得多注册几个账户,以便互相测试是否onwer正确

到目前为止我们大概有90个左右的API,显得非常杂乱,无法一目了然,这就需要我们下一节教程来讲解Open API的使用啦 ^v^