Compare commits
4 Commits
9d23757f45
...
5de299c0c1
Author | SHA1 | Date | |
---|---|---|---|
5de299c0c1 | |||
99cb459fb7 | |||
46aa8312e7 | |||
bbc9c5d6bf |
11
bun.lock
11
bun.lock
@ -4,6 +4,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "3r",
|
"name": "3r",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@casl/ability": "^6.7.3",
|
||||||
"@fastify/static": "^8.2.0",
|
"@fastify/static": "^8.2.0",
|
||||||
"@nestjs/common": "^11.1.3",
|
"@nestjs/common": "^11.1.3",
|
||||||
"@nestjs/core": "^11.1.3",
|
"@nestjs/core": "^11.1.3",
|
||||||
@ -167,6 +168,8 @@
|
|||||||
|
|
||||||
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="],
|
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="],
|
||||||
|
|
||||||
|
"@casl/ability": ["@casl/ability@6.7.3", "", { "dependencies": { "@ucast/mongo2js": "^1.3.0" } }, "sha512-A4L28Ko+phJAsTDhRjzCOZWECQWN2jzZnJPnROWWHjJpyMq1h7h9ZqjwS2WbIUa3Z474X1ZPSgW0f1PboZGC0A=="],
|
||||||
|
|
||||||
"@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="],
|
"@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="],
|
||||||
|
|
||||||
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
|
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
|
||||||
@ -577,6 +580,14 @@
|
|||||||
|
|
||||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.34.1", "", { "dependencies": { "@typescript-eslint/types": "8.34.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw=="],
|
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.34.1", "", { "dependencies": { "@typescript-eslint/types": "8.34.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw=="],
|
||||||
|
|
||||||
|
"@ucast/core": ["@ucast/core@1.10.2", "", {}, "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g=="],
|
||||||
|
|
||||||
|
"@ucast/js": ["@ucast/js@3.0.4", "", { "dependencies": { "@ucast/core": "^1.0.0" } }, "sha512-TgG1aIaCMdcaEyckOZKQozn1hazE0w90SVdlpIJ/er8xVumE11gYAtSbw/LBeUnA4fFnFWTcw3t6reqseeH/4Q=="],
|
||||||
|
|
||||||
|
"@ucast/mongo": ["@ucast/mongo@2.4.3", "", { "dependencies": { "@ucast/core": "^1.4.1" } }, "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA=="],
|
||||||
|
|
||||||
|
"@ucast/mongo2js": ["@ucast/mongo2js@1.4.0", "", { "dependencies": { "@ucast/core": "^1.6.1", "@ucast/js": "^3.0.0", "@ucast/mongo": "^2.4.0" } }, "sha512-vR9RJ3BHlkI3RfKJIZFdVktxWvBCQRiSTeJSWN9NPxP5YJkpfXvcBWAMLwvyJx4HbB+qib5/AlSDEmQiuQyx2w=="],
|
||||||
|
|
||||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||||
|
|
||||||
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.9.0", "", { "os": "android", "cpu": "arm" }, "sha512-h1T2c2Di49ekF2TE8ZCoJkb+jwETKUIPDJ/nO3tJBKlLFPu+fyd93f0rGP/BvArKx2k2HlRM4kqkNarj3dvZlg=="],
|
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.9.0", "", { "os": "android", "cpu": "arm" }, "sha512-h1T2c2Di49ekF2TE8ZCoJkb+jwETKUIPDJ/nO3tJBKlLFPu+fyd93f0rGP/BvArKx2k2HlRM4kqkNarj3dvZlg=="],
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@casl/ability": "^6.7.3",
|
||||||
"@fastify/static": "^8.2.0",
|
"@fastify/static": "^8.2.0",
|
||||||
"@nestjs/common": "^11.1.3",
|
"@nestjs/common": "^11.1.3",
|
||||||
"@nestjs/core": "^11.1.3",
|
"@nestjs/core": "^11.1.3",
|
||||||
|
@ -1 +1,3 @@
|
|||||||
export const DTO_VALIDATION_OPTIONS = 'dto_validation_options';
|
export const DTO_VALIDATION_OPTIONS = 'dto_validation_options';
|
||||||
|
|
||||||
|
export const ADDTIONAL_RELATIONSHIPS = 'addtional_relationships';
|
||||||
|
11
src/modules/core/decorator/dynamic.relationship.decorator.ts
Normal file
11
src/modules/core/decorator/dynamic.relationship.decorator.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { ObjectLiteral } from 'typeorm';
|
||||||
|
|
||||||
|
import { ADDTIONAL_RELATIONSHIPS } from '../contants';
|
||||||
|
import { DynamicRelation } from '../types';
|
||||||
|
|
||||||
|
export function AddRelations(relations: () => Array<DynamicRelation>) {
|
||||||
|
return <T extends ObjectLiteral>(target: T) => {
|
||||||
|
Reflect.defineMetadata(ADDTIONAL_RELATIONSHIPS, relations, target);
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-duplicate-type-constituents */
|
||||||
import { ModuleMetadata, PipeTransform, Type } from '@nestjs/common';
|
import { ModuleMetadata, PipeTransform, Type } from '@nestjs/common';
|
||||||
import { IAuthGuard } from '@nestjs/passport';
|
import { IAuthGuard } from '@nestjs/passport';
|
||||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||||
@ -5,6 +6,7 @@ import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { Ora } from 'ora';
|
import { Ora } from 'ora';
|
||||||
import { StartOptions } from 'pm2';
|
import { StartOptions } from 'pm2';
|
||||||
|
import { ManyToMany, ManyToOne, OneToMany, OneToOne } from 'typeorm';
|
||||||
import { CommandModule } from 'yargs';
|
import { CommandModule } from 'yargs';
|
||||||
|
|
||||||
import { Configure } from '../config/configure';
|
import { Configure } from '../config/configure';
|
||||||
@ -131,3 +133,12 @@ export type CommandCollection = Array<CommandItem<any, any>>;
|
|||||||
export interface CreateOption {
|
export interface CreateOption {
|
||||||
commands: () => CommandCollection;
|
commands: () => CommandCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DynamicRelation {
|
||||||
|
relation:
|
||||||
|
| ReturnType<typeof OneToOne>
|
||||||
|
| ReturnType<typeof OneToMany>
|
||||||
|
| ReturnType<typeof ManyToOne>
|
||||||
|
| ReturnType<typeof ManyToMany>;
|
||||||
|
column: string;
|
||||||
|
}
|
||||||
|
8
src/modules/rbac/constants.ts
Normal file
8
src/modules/rbac/constants.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export enum SystemRoles {
|
||||||
|
USER = 'user',
|
||||||
|
SUPER_ADMIN = 'super_admin',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SYSTEM_PERMISSION = 'system-manage';
|
||||||
|
|
||||||
|
export const PERMISSION_CHECKERS = 'permission_checkers';
|
7
src/modules/rbac/decorators/permission.decorator.ts
Normal file
7
src/modules/rbac/decorators/permission.decorator.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { PERMISSION_CHECKERS } from '../constants';
|
||||||
|
import { PermissionChecker } from '../types';
|
||||||
|
|
||||||
|
export const Permision = (...checkers: PermissionChecker[]) =>
|
||||||
|
SetMetadata(PERMISSION_CHECKERS, checkers);
|
18
src/modules/rbac/dtos/permission.dto.ts
Normal file
18
src/modules/rbac/dtos/permission.dto.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { IsOptional, IsUUID } from 'class-validator';
|
||||||
|
|
||||||
|
import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator';
|
||||||
|
import { IsDataExist } from '@/modules/database/constraints';
|
||||||
|
import { PaginateDto } from '@/modules/restful/dtos/paginate.dto';
|
||||||
|
|
||||||
|
import { RoleEntity } from '../entities/role.entity';
|
||||||
|
|
||||||
|
@DtoValidation({ type: 'query' })
|
||||||
|
export class QueryPermissionDto extends PaginateDto {
|
||||||
|
/**
|
||||||
|
* 角色ID:通过角色过滤权限列表
|
||||||
|
*/
|
||||||
|
@IsDataExist(RoleEntity, { groups: ['update'], message: '指定的角色不存在' })
|
||||||
|
@IsUUID(undefined, { message: '角色ID格式错误' })
|
||||||
|
@IsOptional()
|
||||||
|
role?: string;
|
||||||
|
}
|
56
src/modules/rbac/dtos/role.dtos.ts
Normal file
56
src/modules/rbac/dtos/role.dtos.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { IsDefined, IsNotEmpty, IsOptional, IsUUID, MaxLength } from 'class-validator';
|
||||||
|
|
||||||
|
import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator';
|
||||||
|
import { IsDataExist } from '@/modules/database/constraints';
|
||||||
|
import { PaginateWithTrashedDto } from '@/modules/restful/dtos/paginate-width-trashed.dto';
|
||||||
|
import { UserEntity } from '@/modules/user/entities';
|
||||||
|
|
||||||
|
import { PermissionEntity } from '../entities/permission.entity';
|
||||||
|
|
||||||
|
@DtoValidation({ type: 'query' })
|
||||||
|
export class QueryRoleDto extends PaginateWithTrashedDto {
|
||||||
|
/**
|
||||||
|
* 用户ID:通过用户过滤角色列表
|
||||||
|
*/
|
||||||
|
@IsDataExist(UserEntity, { groups: ['update'], message: '指定的用户不存在' })
|
||||||
|
@IsUUID(undefined, { message: '用户ID格式错误' })
|
||||||
|
@IsOptional()
|
||||||
|
user?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@DtoValidation({ groups: ['create'] })
|
||||||
|
export class CreateRoleDto {
|
||||||
|
/**
|
||||||
|
* 权限名称
|
||||||
|
*/
|
||||||
|
@MaxLength(100, { always: true, message: '名称长度最大为$constraint1' })
|
||||||
|
@IsNotEmpty({ groups: ['create'], message: '名称必须填写' })
|
||||||
|
@IsOptional({ groups: ['update'] })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限标识:如果没有设置则在查询后为权限名称
|
||||||
|
*/
|
||||||
|
@MaxLength(100, { always: true, message: 'Label长度最大为$constraint1' })
|
||||||
|
@IsOptional({ always: true })
|
||||||
|
label?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联权限ID列表:一个角色可以关联多个权限,一个权限也可以属于多个角色
|
||||||
|
*/
|
||||||
|
@IsDataExist(PermissionEntity, { each: true, always: true, message: '权限不存在' })
|
||||||
|
@IsUUID(undefined, { each: true, always: true, message: '权限ID格式不正确' })
|
||||||
|
@IsOptional({ always: true })
|
||||||
|
permissions?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@DtoValidation({ groups: ['update'] })
|
||||||
|
export class UpdateRoleDto extends PartialType(CreateRoleDto) {
|
||||||
|
/**
|
||||||
|
* 待更新的角色ID
|
||||||
|
*/
|
||||||
|
@IsUUID(undefined, { message: 'ID格式错误', groups: ['update'] })
|
||||||
|
@IsDefined({ groups: ['update'], message: 'ID必须指定' })
|
||||||
|
id: string;
|
||||||
|
}
|
66
src/modules/rbac/entities/permission.entity.ts
Normal file
66
src/modules/rbac/entities/permission.entity.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { AbilityTuple, MongoQuery, RawRuleFrom } from '@casl/ability';
|
||||||
|
import { Exclude, Expose } from 'class-transformer';
|
||||||
|
import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn, Relation } from 'typeorm';
|
||||||
|
|
||||||
|
import { UserEntity } from '@/modules/user/entities';
|
||||||
|
|
||||||
|
import { RoleEntity } from './role.entity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限实体
|
||||||
|
*/
|
||||||
|
@Exclude()
|
||||||
|
@Entity('rbac_permission')
|
||||||
|
export class PermissionEntity<
|
||||||
|
P extends AbilityTuple = AbilityTuple,
|
||||||
|
T extends MongoQuery = MongoQuery,
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* 权限ID
|
||||||
|
*/
|
||||||
|
@Expose()
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限名称
|
||||||
|
*/
|
||||||
|
@Expose()
|
||||||
|
@Column({ comment: '权限名称' })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限显示名
|
||||||
|
*/
|
||||||
|
@Expose()
|
||||||
|
@Column({ comment: '权限显示名', nullable: true })
|
||||||
|
label?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限描述
|
||||||
|
*/
|
||||||
|
@Expose()
|
||||||
|
@Column({ comment: '权限描述', nullable: true, type: 'text' })
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限规则
|
||||||
|
*/
|
||||||
|
@Column({ type: 'simple-json', comment: '权限规则' })
|
||||||
|
rule: Omit<RawRuleFrom<P, T>, 'conditions'>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限角色
|
||||||
|
*/
|
||||||
|
@Expose({ groups: ['permission-list', 'permission-detail'] })
|
||||||
|
@ManyToMany(() => RoleEntity, (role) => role.permissions, { cascade: true })
|
||||||
|
@JoinTable()
|
||||||
|
roles: Relation<RoleEntity>[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限用户
|
||||||
|
*/
|
||||||
|
@ManyToMany(() => UserEntity, (user) => user.permissions)
|
||||||
|
@JoinTable()
|
||||||
|
users: Relation<UserEntity>[];
|
||||||
|
}
|
79
src/modules/rbac/entities/role.entity.ts
Normal file
79
src/modules/rbac/entities/role.entity.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { Exclude, Expose, Type } from 'class-transformer';
|
||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Column,
|
||||||
|
DeleteDateColumn,
|
||||||
|
Entity,
|
||||||
|
JoinTable,
|
||||||
|
ManyToMany,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Relation,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
import { UserEntity } from '@/modules/user/entities';
|
||||||
|
|
||||||
|
import { PermissionEntity } from './permission.entity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色信息
|
||||||
|
*/
|
||||||
|
@Exclude()
|
||||||
|
@Entity('rbac_role')
|
||||||
|
export class RoleEntity extends BaseEntity {
|
||||||
|
/**
|
||||||
|
* 角色ID
|
||||||
|
*/
|
||||||
|
@Expose()
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色名称
|
||||||
|
*/
|
||||||
|
@Column({ comment: '角色名称' })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示名称
|
||||||
|
*/
|
||||||
|
@Column({ comment: '显示名称', nullable: true })
|
||||||
|
label?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色描述
|
||||||
|
*/
|
||||||
|
@Column({ comment: '角色描述', nullable: true, type: 'text' })
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为不可更改的系统权限
|
||||||
|
*/
|
||||||
|
@Column({ comment: '是否为不可更改的系统权限', default: false })
|
||||||
|
systemed?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除时间
|
||||||
|
*/
|
||||||
|
@Expose({ groups: ['role-detail', 'role-list'] })
|
||||||
|
@Type(() => Date)
|
||||||
|
@DeleteDateColumn({ comment: '删除时间' })
|
||||||
|
deletedAt: Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色权限
|
||||||
|
*/
|
||||||
|
@Expose({ groups: ['role-detail'] })
|
||||||
|
@Type(() => PermissionEntity)
|
||||||
|
@ManyToMany(() => PermissionEntity, (permission) => permission.roles, {
|
||||||
|
cascade: true,
|
||||||
|
eager: true,
|
||||||
|
})
|
||||||
|
permissions: Relation<PermissionEntity>[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色关联用户
|
||||||
|
*/
|
||||||
|
@ManyToMany(() => UserEntity, (user) => user.roles, { deferrable: 'INITIALLY IMMEDIATE' })
|
||||||
|
@JoinTable()
|
||||||
|
users: Relation<UserEntity>[];
|
||||||
|
}
|
100
src/modules/rbac/guards/rbac.guard.ts
Normal file
100
src/modules/rbac/guards/rbac.guard.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { createMongoAbility } from '@casl/ability';
|
||||||
|
import { ExecutionContext, ForbiddenException } from '@nestjs/common';
|
||||||
|
import { ModuleRef, Reflector } from '@nestjs/core';
|
||||||
|
|
||||||
|
import { isNil } from 'lodash';
|
||||||
|
|
||||||
|
import { JwtAuthGuard } from '@/modules/user/guards';
|
||||||
|
|
||||||
|
import { UserRepository } from '@/modules/user/repositories';
|
||||||
|
import { TokenService } from '@/modules/user/services';
|
||||||
|
|
||||||
|
import { PERMISSION_CHECKERS } from '../constants';
|
||||||
|
import { PermissionEntity } from '../entities/permission.entity';
|
||||||
|
import { RbacResolver } from '../rbac.resolver';
|
||||||
|
|
||||||
|
import { CheckerParams, PermissionChecker } from '../types';
|
||||||
|
|
||||||
|
export class RbacGuard extends JwtAuthGuard {
|
||||||
|
constructor(
|
||||||
|
protected reflector: Reflector,
|
||||||
|
protected resolver: RbacResolver,
|
||||||
|
protected tokenService: TokenService,
|
||||||
|
protected userRepository: UserRepository,
|
||||||
|
protected modeleRef: ModuleRef,
|
||||||
|
) {
|
||||||
|
super(reflector, tokenService);
|
||||||
|
}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const authCheck = await super.canActivate(context);
|
||||||
|
|
||||||
|
if (!authCheck) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkers = this.reflector.getAllAndOverride<PermissionChecker[]>(
|
||||||
|
PERMISSION_CHECKERS,
|
||||||
|
[context.getHandler(), context.getClass()],
|
||||||
|
);
|
||||||
|
if (isNil(checkers) || checkers.length < 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await checkPermissions({
|
||||||
|
resolver: this.resolver,
|
||||||
|
repository: this.userRepository,
|
||||||
|
checkers,
|
||||||
|
moduleRef: this.modeleRef,
|
||||||
|
request: context.switchToHttp().getRequest(),
|
||||||
|
});
|
||||||
|
if (!result) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查权限
|
||||||
|
* @param CheckerParams
|
||||||
|
*/
|
||||||
|
export async function checkPermissions({
|
||||||
|
checkers,
|
||||||
|
moduleRef,
|
||||||
|
resolver,
|
||||||
|
repository,
|
||||||
|
request,
|
||||||
|
}: CheckerParams) {
|
||||||
|
if (isNil(request.user)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const user = await repository.findOneOrFail({
|
||||||
|
relations: ['roles.permissions', 'permissions'],
|
||||||
|
where: { id: request.user.id },
|
||||||
|
});
|
||||||
|
let permissions = user.permissions as PermissionEntity[];
|
||||||
|
for (const role of user.roles) {
|
||||||
|
permissions = [...permissions, ...role.permissions];
|
||||||
|
}
|
||||||
|
permissions = permissions.reduce((o, n) => {
|
||||||
|
if (o.find(({ name }) => name === n.name)) {
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
return [...o, n];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const ability = createMongoAbility(
|
||||||
|
permissions.map(({ rule, name }) => {
|
||||||
|
const resolve = resolver.permissions.find((p) => p.name === name);
|
||||||
|
if (!isNil(resolve) && !isNil(resolve.rule.conditions)) {
|
||||||
|
return { ...rule, conditions: resolve.rule.conditions(user) };
|
||||||
|
}
|
||||||
|
return rule;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const results = await Promise.all(
|
||||||
|
checkers.map(async (checker) => checker(ability, moduleRef, request)),
|
||||||
|
);
|
||||||
|
return results.every((r) => !!r);
|
||||||
|
}
|
267
src/modules/rbac/rbac.resolver.ts
Normal file
267
src/modules/rbac/rbac.resolver.ts
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
import { AbilityOptions, AbilityTuple, MongoQuery, SubjectType } from '@casl/ability';
|
||||||
|
import { InternalServerErrorException, OnApplicationBootstrap } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { isArray, isNil, omit } from 'lodash';
|
||||||
|
import { DataSource, EntityManager, In, Not } from 'typeorm';
|
||||||
|
|
||||||
|
import { Configure } from '../config/configure';
|
||||||
|
|
||||||
|
import { deepMerge } from '../core/helpers';
|
||||||
|
|
||||||
|
import { UserEntity } from '../user/entities';
|
||||||
|
|
||||||
|
import { SYSTEM_PERMISSION, SystemRoles } from './constants';
|
||||||
|
import { PermissionEntity } from './entities/permission.entity';
|
||||||
|
import { RoleEntity } from './entities/role.entity';
|
||||||
|
import { PermissionType, Role } from './types';
|
||||||
|
|
||||||
|
const getSubject = <R extends SubjectType>(subject: R) => {
|
||||||
|
if (typeof subject === 'string') {
|
||||||
|
return subject;
|
||||||
|
}
|
||||||
|
if (subject.modelName) {
|
||||||
|
return subject;
|
||||||
|
}
|
||||||
|
return subject.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class RbacResolver<P extends AbilityTuple = AbilityTuple, T extends MongoQuery = MongoQuery>
|
||||||
|
implements OnApplicationBootstrap
|
||||||
|
{
|
||||||
|
private setuped = false;
|
||||||
|
|
||||||
|
private options: AbilityOptions<P, T>;
|
||||||
|
|
||||||
|
private _roles: Role[] = [
|
||||||
|
{
|
||||||
|
name: SystemRoles.USER,
|
||||||
|
label: '普通用户',
|
||||||
|
description: '新用户的默认角色',
|
||||||
|
permissions: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: SystemRoles.SUPER_ADMIN,
|
||||||
|
label: '超级管理员',
|
||||||
|
description: '拥有整个系统的管理权限',
|
||||||
|
permissions: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
private _permissions: PermissionType<P, T>[] = [
|
||||||
|
{
|
||||||
|
name: SYSTEM_PERMISSION,
|
||||||
|
label: '系统管理',
|
||||||
|
description: '管理系统的所有功能',
|
||||||
|
rule: {
|
||||||
|
action: 'manage',
|
||||||
|
subject: 'all',
|
||||||
|
} as any,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected dataSource: DataSource,
|
||||||
|
protected configure: Configure,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
setOptions(options: AbilityOptions<P, T>) {
|
||||||
|
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: PermissionType<P, T>[]) {
|
||||||
|
this._permissions = [...this._permissions, ...data].map((perm) => {
|
||||||
|
let subject: typeof perm.rule.subject;
|
||||||
|
if (isArray(perm.rule.subject)) {
|
||||||
|
subject = perm.rule.subject.map((v) => getSubject(v));
|
||||||
|
} else {
|
||||||
|
subject = getSubject(perm.rule.subject);
|
||||||
|
}
|
||||||
|
const rule = { ...perm.rule, subject };
|
||||||
|
return { ...perm, rule };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async onApplicationBootstrap() {
|
||||||
|
if (!this.dataSource.isInitialized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.syncRoles(queryRunner.manager);
|
||||||
|
await this.syncPermissions(queryRunner.manager);
|
||||||
|
await this.syncSuperAdmin(queryRunner.manager);
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步角色
|
||||||
|
* @param manager
|
||||||
|
*/
|
||||||
|
async syncRoles(manager: EntityManager) {
|
||||||
|
this._roles = this.roles.reduce((o, n) => {
|
||||||
|
if (o.map(({ name }) => name).includes(n.name)) {
|
||||||
|
return o.map((e) => (e.name === n.name ? deepMerge(e, n, 'merge') : e));
|
||||||
|
}
|
||||||
|
return [...o, n];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
for (const item of this.roles) {
|
||||||
|
let role = await manager.findOne(RoleEntity, {
|
||||||
|
relations: ['permissions'],
|
||||||
|
where: { name: item.name },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isNil(role)) {
|
||||||
|
role = await manager.save(
|
||||||
|
manager.create(RoleEntity, {
|
||||||
|
name: item.name,
|
||||||
|
label: item.label,
|
||||||
|
description: item.description,
|
||||||
|
systemed: true,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
reload: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await manager.update(RoleEntity, role.id, { systemed: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemRoles = await manager.findBy(RoleEntity, { systemed: true });
|
||||||
|
const toDels: string[] = [];
|
||||||
|
for (const item of systemRoles) {
|
||||||
|
if (isNil(this.roles.find(({ name }) => item.name === name))) {
|
||||||
|
toDels.push(item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toDels.length > 0) {
|
||||||
|
await manager.delete(RoleEntity, toDels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncPermissions(manager: EntityManager) {
|
||||||
|
const permissions = await manager.find(PermissionEntity);
|
||||||
|
const roles = await manager.find(RoleEntity, {
|
||||||
|
relations: ['permissions'],
|
||||||
|
where: { name: Not(SystemRoles.SUPER_ADMIN) },
|
||||||
|
});
|
||||||
|
const roleRepo = manager.getRepository(RoleEntity);
|
||||||
|
|
||||||
|
// 合并并去除重复权限
|
||||||
|
this._permissions = this.permissions.reduce(
|
||||||
|
(o, n) => (o.map(({ name }) => name).includes(n.name) ? o : [...o, n]),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const names = this.permissions.map(({ name }) => name);
|
||||||
|
|
||||||
|
for (const item of this.permissions) {
|
||||||
|
const perm = omit(item, ['conditions']);
|
||||||
|
const old = await manager.findOneBy(PermissionEntity, { name: perm.name });
|
||||||
|
if (isNil(old)) {
|
||||||
|
await manager.save(manager.create(PermissionEntity, perm));
|
||||||
|
} else {
|
||||||
|
await manager.update(PermissionEntity, old.id, perm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除冗余权限
|
||||||
|
const toDels: string[] = [];
|
||||||
|
for (const item of permissions) {
|
||||||
|
if (!names.includes(item.name) && item.name !== SYSTEM_PERMISSION) {
|
||||||
|
toDels.push(item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toDels.length > 0) {
|
||||||
|
await manager.delete(PermissionEntity, toDels);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步普通角色
|
||||||
|
for (const role of roles) {
|
||||||
|
const rolePermissions = await manager.findBy(PermissionEntity, {
|
||||||
|
name: In(this.roles.find(({ name }) => name === role.name).permissions),
|
||||||
|
});
|
||||||
|
await roleRepo
|
||||||
|
.createQueryBuilder('role')
|
||||||
|
.relation(RoleEntity, 'permissions')
|
||||||
|
.of(role)
|
||||||
|
.addAndRemove(
|
||||||
|
rolePermissions.map(({ id }) => id),
|
||||||
|
(role.permissions ?? []).map(({ id }) => id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步超级管理员角色
|
||||||
|
const superRole = await manager.findOneOrFail(RoleEntity, {
|
||||||
|
relations: ['permissions'],
|
||||||
|
where: { name: SystemRoles.SUPER_ADMIN },
|
||||||
|
});
|
||||||
|
const systemManage = await manager.findOneOrFail(PermissionEntity, {
|
||||||
|
where: { name: SYSTEM_PERMISSION },
|
||||||
|
});
|
||||||
|
await roleRepo
|
||||||
|
.createQueryBuilder('role')
|
||||||
|
.relation(RoleEntity, 'permissions')
|
||||||
|
.of(superRole)
|
||||||
|
.addAndRemove(
|
||||||
|
[systemManage.id],
|
||||||
|
(superRole.permissions ?? []).map(({ id }) => id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncSuperAdmin(manager: EntityManager) {
|
||||||
|
const superRole = await manager.findOneOrFail(RoleEntity, {
|
||||||
|
relations: ['permissions'],
|
||||||
|
where: { name: SystemRoles.SUPER_ADMIN },
|
||||||
|
});
|
||||||
|
const superUsers = await manager
|
||||||
|
.createQueryBuilder(UserEntity, 'user')
|
||||||
|
.leftJoinAndSelect('user.roles', 'roles')
|
||||||
|
.where('roles.id IN (:...ids', { ids: [superRole.id] })
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
if (superUsers.length < 1) {
|
||||||
|
const userRepo = manager.getRepository(UserEntity);
|
||||||
|
if ((await userRepo.count()) < 1) {
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
'Please add a super-admin user first before run server!',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstUser = await userRepo.findOneByOrFail({ id: undefined });
|
||||||
|
await userRepo
|
||||||
|
.createQueryBuilder('user')
|
||||||
|
.relation(UserEntity, 'roles')
|
||||||
|
.of(firstUser)
|
||||||
|
.addAndRemove(
|
||||||
|
[superRole.id],
|
||||||
|
(firstUser.roles ?? []).map(({ id }) => id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
src/modules/rbac/repositories/permission.repository.ts
Normal file
21
src/modules/rbac/repositories/permission.repository.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { AbilityTuple, MongoQuery } from '@casl/ability';
|
||||||
|
|
||||||
|
import { SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
|
import { BaseRepository } from '@/modules/database/base/repository';
|
||||||
|
|
||||||
|
import { CustomRepository } from '@/modules/database/decorators/repository.decorator';
|
||||||
|
|
||||||
|
import { PermissionEntity } from '../entities/permission.entity';
|
||||||
|
|
||||||
|
@CustomRepository(PermissionEntity)
|
||||||
|
export class PermissionRepository extends BaseRepository<PermissionEntity> {
|
||||||
|
protected _qbName: string = 'permission';
|
||||||
|
|
||||||
|
buildBaseQB(): SelectQueryBuilder<PermissionEntity<AbilityTuple, MongoQuery>> {
|
||||||
|
return this.createQueryBuilder(this.qbName).leftJoinAndSelect(
|
||||||
|
`${this.qbName}.roles`,
|
||||||
|
'roles',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
19
src/modules/rbac/repositories/role.repository.ts
Normal file
19
src/modules/rbac/repositories/role.repository.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
|
import { BaseRepository } from '@/modules/database/base/repository';
|
||||||
|
|
||||||
|
import { CustomRepository } from '@/modules/database/decorators/repository.decorator';
|
||||||
|
|
||||||
|
import { RoleEntity } from '../entities/role.entity';
|
||||||
|
|
||||||
|
@CustomRepository(RoleEntity)
|
||||||
|
export class RoleRepository extends BaseRepository<RoleEntity> {
|
||||||
|
protected _qbName: string = 'role';
|
||||||
|
|
||||||
|
buildBaseQB(): SelectQueryBuilder<RoleEntity> {
|
||||||
|
return this.createQueryBuilder(this.qbName).leftJoinAndSelect(
|
||||||
|
`${this.qbName}.permissions`,
|
||||||
|
'permissions',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
48
src/modules/rbac/services/permission.service.ts
Normal file
48
src/modules/rbac/services/permission.service.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { AbilityTuple, MongoQuery } from '@casl/ability';
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { isNil } from 'lodash';
|
||||||
|
import { SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
|
import { BaseService } from '@/modules/database/base/service';
|
||||||
|
|
||||||
|
import { QueryHook } from '@/modules/database/types';
|
||||||
|
|
||||||
|
import { QueryPermissionDto } from '../dtos/permission.dto';
|
||||||
|
import { PermissionEntity } from '../entities/permission.entity';
|
||||||
|
import { PermissionRepository } from '../repositories/permission.repository';
|
||||||
|
|
||||||
|
type FindParams = {
|
||||||
|
[key in keyof Omit<QueryPermissionDto, 'limit' | 'page'>]: QueryPermissionDto[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PermissionService extends BaseService<
|
||||||
|
PermissionEntity,
|
||||||
|
PermissionRepository,
|
||||||
|
FindParams
|
||||||
|
> {
|
||||||
|
constructor(protected permissionRepository: PermissionRepository) {
|
||||||
|
super(permissionRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async buildListQuery(
|
||||||
|
queryBuilder: SelectQueryBuilder<PermissionEntity>,
|
||||||
|
options: FindParams,
|
||||||
|
callback?: QueryHook<PermissionEntity>,
|
||||||
|
) {
|
||||||
|
const qb = await super.buildListQB(queryBuilder, options, callback);
|
||||||
|
if (!isNil(options.role)) {
|
||||||
|
qb.andWhere('role.id IN (:...roles', { roles: [options.role] });
|
||||||
|
}
|
||||||
|
return qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
create(data: PermissionEntity): Promise<PermissionEntity<AbilityTuple, MongoQuery>> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
update(data: PermissionEntity): Promise<PermissionEntity<AbilityTuple, MongoQuery>> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
}
|
81
src/modules/rbac/services/role.service.ts
Normal file
81
src/modules/rbac/services/role.service.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { ForbiddenException, Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { isNil, omit } from 'lodash';
|
||||||
|
import { In, SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
|
import { BaseService } from '@/modules/database/base/service';
|
||||||
|
|
||||||
|
import { QueryHook } from '@/modules/database/types';
|
||||||
|
|
||||||
|
import { CreateRoleDto, QueryRoleDto, UpdateRoleDto } from '../dtos/role.dtos';
|
||||||
|
import { RoleEntity } from '../entities/role.entity';
|
||||||
|
import { PermissionRepository } from '../repositories/permission.repository';
|
||||||
|
import { RoleRepository } from '../repositories/role.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RoleService extends BaseService<RoleEntity, RoleRepository> {
|
||||||
|
protected enableTrash = true;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected roleRepository: RoleRepository,
|
||||||
|
protected permissionRepository: PermissionRepository,
|
||||||
|
) {
|
||||||
|
super(roleRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: CreateRoleDto): Promise<RoleEntity> {
|
||||||
|
const createRole = {
|
||||||
|
...data,
|
||||||
|
permissions: data.permissions
|
||||||
|
? await this.permissionRepository.findBy({ id: In(data.permissions) })
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
const item = await this.repository.save(createRole);
|
||||||
|
return this.detail(item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(data: UpdateRoleDto): Promise<RoleEntity> {
|
||||||
|
const role = await this.detail(data.id);
|
||||||
|
if (data.permissions) {
|
||||||
|
await this.repository
|
||||||
|
.createQueryBuilder('role')
|
||||||
|
.relation(RoleEntity, 'permissions')
|
||||||
|
.of(role)
|
||||||
|
.addAndRemove(data.permissions, role.permissions ?? []);
|
||||||
|
}
|
||||||
|
await this.repository.update(data.id, omit(data, ['id', 'permissions']));
|
||||||
|
return this.detail(data.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(items: string[], trash = true): Promise<RoleEntity[]> {
|
||||||
|
const roles = await this.repository.find({ where: { id: In(items) }, withDeleted: true });
|
||||||
|
for (const role of roles) {
|
||||||
|
if (role.systemed) {
|
||||||
|
throw new ForbiddenException('can not remove systemed role!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!trash) {
|
||||||
|
return this.repository.remove(roles);
|
||||||
|
}
|
||||||
|
const directs = roles.filter((item) => !isNil(item.deletedAt));
|
||||||
|
const softs = roles.filter((item) => isNil(item.deletedAt));
|
||||||
|
return [
|
||||||
|
...(await this.repository.remove(directs)),
|
||||||
|
...(await this.repository.softRemove(softs)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async buildListQuery(
|
||||||
|
queryBuilder: SelectQueryBuilder<RoleEntity>,
|
||||||
|
options: QueryRoleDto,
|
||||||
|
callback?: QueryHook<RoleEntity>,
|
||||||
|
) {
|
||||||
|
const qb = await super.buildListQB(queryBuilder, options, callback);
|
||||||
|
qb.leftJoinAndSelect(`${this.repository.qbName}.users`, 'users');
|
||||||
|
if (!isNil(options.user)) {
|
||||||
|
qb.andWhere('users.id IN (:...users)', { users: [options.user] });
|
||||||
|
}
|
||||||
|
return qb;
|
||||||
|
}
|
||||||
|
}
|
15
src/modules/rbac/subscribers/permission.subscriber.ts
Normal file
15
src/modules/rbac/subscribers/permission.subscriber.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { isNil } from 'lodash';
|
||||||
|
|
||||||
|
import { BaseSubscriber } from '@/modules/database/base/subscriber';
|
||||||
|
|
||||||
|
import { PermissionEntity } from '../entities/permission.entity';
|
||||||
|
|
||||||
|
export class PermissionSubscriber extends BaseSubscriber<PermissionEntity> {
|
||||||
|
protected entity = PermissionEntity;
|
||||||
|
|
||||||
|
async afterLoad(entity: PermissionEntity): Promise<void> {
|
||||||
|
if (isNil(entity.label)) {
|
||||||
|
entity.label = entity.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
src/modules/rbac/subscribers/role.subscriber.ts
Normal file
15
src/modules/rbac/subscribers/role.subscriber.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { isNil } from 'lodash';
|
||||||
|
|
||||||
|
import { BaseSubscriber } from '@/modules/database/base/subscriber';
|
||||||
|
|
||||||
|
import { RoleEntity } from '../entities/role.entity';
|
||||||
|
|
||||||
|
export class RoleSubscriber extends BaseSubscriber<RoleEntity> {
|
||||||
|
protected entity = RoleEntity;
|
||||||
|
|
||||||
|
async afterLoad(entity: RoleEntity): Promise<void> {
|
||||||
|
if (isNil(entity.label)) {
|
||||||
|
entity.label = entity.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
src/modules/rbac/types.ts
Normal file
42
src/modules/rbac/types.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import { AbilityTuple, MongoAbility, MongoQuery, RawRuleFrom } from '@casl/ability';
|
||||||
|
|
||||||
|
import { ModuleRef } from '@nestjs/core';
|
||||||
|
|
||||||
|
import { FastifyRequest as Request } from 'fastify';
|
||||||
|
|
||||||
|
import { UserEntity } from '../user/entities';
|
||||||
|
|
||||||
|
import { UserRepository } from '../user/repositories';
|
||||||
|
|
||||||
|
import { PermissionEntity } from './entities/permission.entity';
|
||||||
|
import { RoleEntity } from './entities/role.entity';
|
||||||
|
import { RbacResolver } from './rbac.resolver';
|
||||||
|
|
||||||
|
export type Role = Pick<ClassToPlain<RoleEntity>, 'name' | 'label' | 'description'> & {
|
||||||
|
permissions: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PermissionType<P extends AbilityTuple, T extends MongoQuery> = Pick<
|
||||||
|
ClassToPlain<PermissionEntity<P, T>>,
|
||||||
|
'name'
|
||||||
|
> &
|
||||||
|
Partial<Pick<ClassToPlain<PermissionEntity<P, T>>, 'label' | 'description'>> & {
|
||||||
|
rule: Omit<RawRuleFrom<P, T>, 'conditions'> & {
|
||||||
|
conditions?: (user: ClassToPlain<UserEntity>) => RecordAny;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PermissionChecker = (
|
||||||
|
ability: MongoAbility,
|
||||||
|
ref?: ModuleRef,
|
||||||
|
request?: Request,
|
||||||
|
) => Promise<boolean>;
|
||||||
|
|
||||||
|
export type CheckerParams = {
|
||||||
|
resolver: RbacResolver;
|
||||||
|
repository: UserRepository;
|
||||||
|
checkers: PermissionChecker[];
|
||||||
|
moduleRef?: ModuleRef;
|
||||||
|
request?: any;
|
||||||
|
};
|
@ -5,12 +5,15 @@ import {
|
|||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
DeleteDateColumn,
|
DeleteDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
ManyToMany,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
PrimaryColumn,
|
PrimaryColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import { CommentEntity, PostEntity } from '@/modules/content/entities';
|
import { CommentEntity, PostEntity } from '@/modules/content/entities';
|
||||||
|
import { PermissionEntity } from '@/modules/rbac/entities/permission.entity';
|
||||||
|
import { RoleEntity } from '@/modules/rbac/entities/role.entity';
|
||||||
import { AccessTokenEntity } from '@/modules/user/entities/access.token.entity';
|
import { AccessTokenEntity } from '@/modules/user/entities/access.token.entity';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -101,4 +104,18 @@ export class UserEntity {
|
|||||||
*/
|
*/
|
||||||
@OneToMany(() => AccessTokenEntity, (token) => token.user, { cascade: true })
|
@OneToMany(() => AccessTokenEntity, (token) => token.user, { cascade: true })
|
||||||
accessTokens: Relation<AccessTokenEntity>[];
|
accessTokens: Relation<AccessTokenEntity>[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户权限
|
||||||
|
*/
|
||||||
|
@Expose()
|
||||||
|
@ManyToMany(() => PermissionEntity, (permission) => permission.users, { cascade: true })
|
||||||
|
permissions: Relation<PermissionEntity>[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户角色
|
||||||
|
*/
|
||||||
|
@Expose()
|
||||||
|
@ManyToMany(() => RoleEntity, (role) => role.users, { cascade: true })
|
||||||
|
roles: Relation<RoleEntity>[];
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user