add rbac module
This commit is contained in:
parent
fab90132b0
commit
9461fc53f3
@ -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;
|
||||
}
|
||||
|
@ -1,2 +1 @@
|
||||
export * from './permission.controller';
|
||||
export * from './role.controller';
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './role.controller';
|
||||
export * from './permission.controller';
|
||||
|
1
src/modules/user/controllers/manager/index.ts
Normal file
1
src/modules/user/controllers/manager/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './user.controller';
|
102
src/modules/user/controllers/manager/user.controller.ts
Normal file
102
src/modules/user/controllers/manager/user.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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()) }));
|
||||
}
|
||||
}
|
||||
|
@ -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']) {}
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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 };
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user