Compare commits
4 Commits
5de299c0c1
...
9461fc53f3
Author | SHA1 | Date | |
---|---|---|---|
9461fc53f3 | |||
fab90132b0 | |||
bff2c4a4c7 | |||
23c8eba867 |
@ -40,6 +40,7 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"deepmerge": "^4.3.1",
|
||||
"dotenv": "^16.5.0",
|
||||
"fastify": "^5.4.0",
|
||||
"find-up": "^7.0.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
|
1251
pnpm-lock.yaml
1251
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
|
@ -6,3 +6,12 @@ export enum SystemRoles {
|
||||
export const SYSTEM_PERMISSION = 'system-manage';
|
||||
|
||||
export const PERMISSION_CHECKERS = 'permission_checkers';
|
||||
|
||||
export enum PermissionAction {
|
||||
CREATE = 'create',
|
||||
READ = 'read',
|
||||
UPDATE = 'update',
|
||||
DELETE = 'delete',
|
||||
MANAGE = 'manage',
|
||||
OWNER = 'onwer',
|
||||
}
|
||||
|
1
src/modules/rbac/controllers/index.ts
Normal file
1
src/modules/rbac/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './role.controller';
|
2
src/modules/rbac/controllers/manager/index.ts
Normal file
2
src/modules/rbac/controllers/manager/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './role.controller';
|
||||
export * from './permission.controller';
|
@ -0,0 +1,47 @@
|
||||
import { Controller, Get, Param, ParseUUIDPipe, Query, SerializeOptions } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { PermissionAction } from '@/modules/rbac/constants';
|
||||
import { Permission } from '@/modules/rbac/decorators/permission.decorator';
|
||||
import { PermissionEntity } from '@/modules/rbac/entities';
|
||||
import { RbacModule } from '@/modules/rbac/rbac.module';
|
||||
import { PermissionService } from '@/modules/rbac/services';
|
||||
import { PermissionChecker } from '@/modules/rbac/types';
|
||||
import { Depends } from '@/modules/restful/decorators/depend.decorator';
|
||||
import { PaginateWithTrashedDto } from '@/modules/restful/dtos/paginate-width-trashed.dto';
|
||||
|
||||
const permission: PermissionChecker = async (ab) =>
|
||||
ab.can(PermissionAction.MANAGE, PermissionEntity.name);
|
||||
|
||||
@ApiTags('权限查询')
|
||||
@ApiBearerAuth()
|
||||
@Depends(RbacModule)
|
||||
@Controller('permissions')
|
||||
export class PermissionController {
|
||||
constructor(private service: PermissionService) {}
|
||||
|
||||
permission: PermissionChecker = async (ab) =>
|
||||
ab.can(PermissionAction.MANAGE, PermissionEntity.name);
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
* @param options
|
||||
*/
|
||||
@Get()
|
||||
@SerializeOptions({ groups: ['permission-list'] })
|
||||
@Permission(permission)
|
||||
async list(@Query() options: PaginateWithTrashedDto) {
|
||||
return this.service.paginate(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
* @param id
|
||||
*/
|
||||
@Get(':id')
|
||||
@SerializeOptions({ groups: ['permission-detail'] })
|
||||
@Permission(permission)
|
||||
async detail(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||
return this.service.detail(id);
|
||||
}
|
||||
}
|
82
src/modules/rbac/controllers/manager/role.controller.ts
Normal file
82
src/modules/rbac/controllers/manager/role.controller.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { Body, Controller, Delete, Patch, Post, 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 { RoleEntity } from '@/modules/rbac/entities';
|
||||
import { RbacModule } from '@/modules/rbac/rbac.module';
|
||||
import { RoleService } from '@/modules/rbac/services';
|
||||
import { PermissionChecker } from '@/modules/rbac/types';
|
||||
import { Depends } from '@/modules/restful/decorators/depend.decorator';
|
||||
|
||||
import { Permission } from '../../decorators/permission.decorator';
|
||||
import { CreateRoleDto, UpdateRoleDto } from '../../dtos/role.dtos';
|
||||
|
||||
const permission: PermissionChecker = async (ab) =>
|
||||
ab.can(PermissionAction.MANAGE, RoleEntity.name);
|
||||
|
||||
@ApiTags('角色管理')
|
||||
@ApiBearerAuth()
|
||||
@Depends(RbacModule)
|
||||
@Controller('roles')
|
||||
export class RoleController {
|
||||
constructor(private service: RoleService) {}
|
||||
|
||||
/**
|
||||
* 新增角色
|
||||
* @param data
|
||||
*/
|
||||
@Post()
|
||||
@SerializeOptions({ groups: ['role-detail'] })
|
||||
@Permission(permission)
|
||||
async store(
|
||||
@Body()
|
||||
data: CreateRoleDto,
|
||||
) {
|
||||
return this.service.create(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新角色
|
||||
* @param data
|
||||
*/
|
||||
@Patch()
|
||||
@SerializeOptions({ groups: ['role-detail'] })
|
||||
@Permission(permission)
|
||||
async update(
|
||||
@Body()
|
||||
data: UpdateRoleDto,
|
||||
) {
|
||||
return this.service.update(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除角色
|
||||
* @param data
|
||||
*/
|
||||
@Delete()
|
||||
@SerializeOptions({ groups: ['role-list'] })
|
||||
@Permission(permission)
|
||||
async delete(
|
||||
@Body()
|
||||
data: DeleteWithTrashDto,
|
||||
) {
|
||||
const { ids, trash } = data;
|
||||
return this.service.delete(ids, trash);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量恢复角色
|
||||
* @param data
|
||||
*/
|
||||
@Patch('restore')
|
||||
@SerializeOptions({ groups: ['role-list'] })
|
||||
@Permission(permission)
|
||||
async restore(
|
||||
@Body()
|
||||
data: RestoreDto,
|
||||
) {
|
||||
const { ids } = data;
|
||||
return this.service.restore(ids);
|
||||
}
|
||||
}
|
37
src/modules/rbac/controllers/role.controller.ts
Normal file
37
src/modules/rbac/controllers/role.controller.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { Controller, Get, Param, ParseUUIDPipe, Query, SerializeOptions } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { RbacModule } from '@/modules/rbac/rbac.module';
|
||||
import { RoleService } from '@/modules/rbac/services';
|
||||
import { Depends } from '@/modules/restful/decorators/depend.decorator';
|
||||
import { PaginateWithTrashedDto } from '@/modules/restful/dtos/paginate-width-trashed.dto';
|
||||
import { Guest } from '@/modules/user/decorators/guest.decorator';
|
||||
|
||||
@ApiTags('角色查询')
|
||||
@Depends(RbacModule)
|
||||
@Controller('roles')
|
||||
export class RoleController {
|
||||
constructor(private service: RoleService) {}
|
||||
|
||||
/**
|
||||
* 角色列表查询
|
||||
* @param options
|
||||
*/
|
||||
@Get()
|
||||
@SerializeOptions({ groups: ['role-list'] })
|
||||
@Guest()
|
||||
async list(@Query() options: PaginateWithTrashedDto) {
|
||||
return this.service.paginate(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色详解查询
|
||||
* @param id
|
||||
*/
|
||||
@Get(':id')
|
||||
@SerializeOptions({ groups: ['role-detail'] })
|
||||
@Guest()
|
||||
async detail(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||
return this.service.detail(id);
|
||||
}
|
||||
}
|
@ -3,5 +3,5 @@ import { SetMetadata } from '@nestjs/common';
|
||||
import { PERMISSION_CHECKERS } from '../constants';
|
||||
import { PermissionChecker } from '../types';
|
||||
|
||||
export const Permision = (...checkers: PermissionChecker[]) =>
|
||||
export const Permission = (...checkers: PermissionChecker[]) =>
|
||||
SetMetadata(PERMISSION_CHECKERS, checkers);
|
||||
|
2
src/modules/rbac/entities/index.ts
Normal file
2
src/modules/rbac/entities/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './permission.entity';
|
||||
export * from './role.entity';
|
52
src/modules/rbac/rbac.module.ts
Normal file
52
src/modules/rbac/rbac.module.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { DynamicModule, forwardRef, Module } from '@nestjs/common';
|
||||
|
||||
import { getDataSourceToken } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { DatabaseModule } from '@/modules/database/database.module';
|
||||
|
||||
import { RbacGuard } from '@/modules/rbac/guards/rbac.guard';
|
||||
|
||||
import { RbacResolver } from '@/modules/rbac/rbac.resolver';
|
||||
|
||||
import { Configure } from '../config/configure';
|
||||
import { addEntities, addSubscribers } from '../database/utils';
|
||||
import { UserModule } from '../user/user.module';
|
||||
|
||||
import * as entities from './entities';
|
||||
import * as repositories from './repositories';
|
||||
import * as services from './services';
|
||||
import * as subscribers from './subscribers';
|
||||
|
||||
@Module({})
|
||||
export class RbacModule {
|
||||
static async forRoot(configure: Configure): Promise<DynamicModule> {
|
||||
return {
|
||||
module: RbacModule,
|
||||
imports: [
|
||||
forwardRef(() => UserModule),
|
||||
await addEntities(configure, Object.values(entities)),
|
||||
DatabaseModule.forRepository(Object.values(repositories)),
|
||||
],
|
||||
providers: [
|
||||
...Object.values(services),
|
||||
...(await addSubscribers(configure, Object.values(subscribers))),
|
||||
RbacGuard,
|
||||
{
|
||||
provide: RbacResolver,
|
||||
useFactory: async (dataSource: DataSource) => {
|
||||
const resolver = new RbacResolver(dataSource, configure);
|
||||
resolver.setOptions({});
|
||||
return resolver;
|
||||
},
|
||||
inject: [getDataSourceToken()],
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
RbacResolver,
|
||||
...Object.values(services),
|
||||
DatabaseModule.forRepository(Object.values(repositories)),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
@ -68,6 +68,7 @@ export class RbacResolver<P extends AbilityTuple = AbilityTuple, T extends Mongo
|
||||
if (!this.setuped) {
|
||||
this.options = options;
|
||||
this.setuped = true;
|
||||
console.log(this.options);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
2
src/modules/rbac/repositories/index.ts
Normal file
2
src/modules/rbac/repositories/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './permission.repository';
|
||||
export * from './role.repository';
|
31
src/modules/rbac/routes.ts
Normal file
31
src/modules/rbac/routes.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { RouteOption, TagOption } from '@/modules/restful/types';
|
||||
|
||||
import * as controllers from './controllers';
|
||||
import * as manageControllers from './controllers/manager';
|
||||
|
||||
export const createRbacApi = () => {
|
||||
const routes: Record<'app' | 'manage', RouteOption[]> = {
|
||||
app: [
|
||||
{
|
||||
name: 'app.rbac',
|
||||
path: 'rbac',
|
||||
controllers: Object.values(controllers),
|
||||
},
|
||||
],
|
||||
manage: [
|
||||
{
|
||||
name: 'manage.rbac',
|
||||
path: 'rbac',
|
||||
controllers: Object.values(manageControllers),
|
||||
},
|
||||
],
|
||||
};
|
||||
const tags: Record<'app' | 'manage', Array<string | TagOption>> = {
|
||||
app: [{ name: '角色查询', description: '查询角色信息' }],
|
||||
manage: [
|
||||
{ name: '角色管理', description: '管理角色信息' },
|
||||
{ name: '权限信息', description: '查询权限信息' },
|
||||
],
|
||||
};
|
||||
return { routes, tags };
|
||||
};
|
2
src/modules/rbac/services/index.ts
Normal file
2
src/modules/rbac/services/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './permission.service';
|
||||
export * from './role.service';
|
2
src/modules/rbac/subscribers/index.ts
Normal file
2
src/modules/rbac/subscribers/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './permission.subscriber';
|
||||
export * from './role.subscriber';
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { AbilityTuple, MongoAbility, MongoQuery, RawRuleFrom } from '@casl/ability';
|
||||
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
|
23
src/modules/rbac/utils.ts
Normal file
23
src/modules/rbac/utils.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { MongoAbility } from '@casl/ability';
|
||||
import { FastifyRequest as Request } from 'fastify';
|
||||
import { ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { PermissionAction } from './constants';
|
||||
|
||||
function getRequestData(request: Request, key: string): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function checkOwnerPermission<T extends ObjectLiteral>(
|
||||
ability: MongoAbility,
|
||||
options: {
|
||||
request: Request;
|
||||
key?: string;
|
||||
getData: (items: string[]) => Promise<T[]>;
|
||||
permission?: string;
|
||||
},
|
||||
) {
|
||||
const { request, key, getData, permission } = options;
|
||||
const models = await getData(getRequestData(request, key));
|
||||
return models.every((model) => ability.can(permission ?? PermissionAction.OWNER, model));
|
||||
}
|
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']) {}
|
||||
|
14
src/modules/user/repositories/access.token.repository.ts
Normal file
14
src/modules/user/repositories/access.token.repository.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
import { BaseRepository } from '@/modules/database/base/repository';
|
||||
import { CustomRepository } from '@/modules/database/decorators/repository.decorator';
|
||||
import { AccessTokenEntity } from '@/modules/user/entities';
|
||||
|
||||
@CustomRepository(AccessTokenEntity)
|
||||
export class AccessTokenRepository extends BaseRepository<AccessTokenEntity> {
|
||||
protected _qbName: string = 'accessToken';
|
||||
|
||||
buildBaseQB(): SelectQueryBuilder<AccessTokenEntity> {
|
||||
return super.createQueryBuilder(this.qbName).orderBy(`${this.qbName}.createdAt`, 'DESC');
|
||||
}
|
||||
}
|
@ -1 +1,3 @@
|
||||
export * from './user.repository';
|
||||
export * from './access.token.repository';
|
||||
export * from './refresh.token.repository';
|
||||
|
14
src/modules/user/repositories/refresh.token.repository.ts
Normal file
14
src/modules/user/repositories/refresh.token.repository.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
import { BaseRepository } from '@/modules/database/base/repository';
|
||||
import { CustomRepository } from '@/modules/database/decorators/repository.decorator';
|
||||
import { RefreshTokenEntity } from '@/modules/user/entities';
|
||||
|
||||
@CustomRepository(RefreshTokenEntity)
|
||||
export class RefreshTokenRepository extends BaseRepository<RefreshTokenEntity> {
|
||||
protected _qbName: string = 'refreshToken';
|
||||
|
||||
buildBaseQB(): SelectQueryBuilder<RefreshTokenEntity> {
|
||||
return super.createQueryBuilder(this.qbName).orderBy(`${this.qbName}.createdAt`, 'DESC');
|
||||
}
|
||||
}
|
@ -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,4 +1,3 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { ForbiddenException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { FastifyRequest as Request } from 'fastify';
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { JwtModule, JwtModuleOptions, JwtService } from '@nestjs/jwt';
|
||||
@ -14,6 +13,7 @@ import { defaultUserConfig, getUserConfig } from '@/modules/user/config';
|
||||
import { AccessTokenEntity } from '@/modules/user/entities/access.token.entity';
|
||||
import { RefreshTokenEntity } from '@/modules/user/entities/refresh.token.entity';
|
||||
import { UserEntity } from '@/modules/user/entities/user.entity';
|
||||
import { AccessTokenRepository, RefreshTokenRepository } from '@/modules/user/repositories';
|
||||
import { JwtConfig, JwtPayload, UserConfig } from '@/modules/user/types';
|
||||
|
||||
import { TokenConst } from '../constants';
|
||||
@ -26,6 +26,8 @@ export class TokenService {
|
||||
constructor(
|
||||
protected configure: Configure,
|
||||
protected jwtService: JwtService,
|
||||
private accessTokenRepository: AccessTokenRepository,
|
||||
private refreshTokenRepository: RefreshTokenRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -41,7 +43,7 @@ export class TokenService {
|
||||
return null;
|
||||
}
|
||||
const token = await this.generateAccessToken(user, now);
|
||||
await accessToken.remove();
|
||||
await this.accessTokenRepository.remove(accessToken);
|
||||
response.header('token', token.accessToken.value);
|
||||
return token;
|
||||
}
|
||||
@ -65,7 +67,8 @@ export class TokenService {
|
||||
accessToken.value = signed;
|
||||
accessToken.user = user;
|
||||
accessToken.expiredAt = now.add(config.tokenExpired, 'second').toDate();
|
||||
await accessToken.save();
|
||||
|
||||
await this.accessTokenRepository.save(accessToken);
|
||||
const refreshToken = await this.generateRefreshToken(
|
||||
accessToken,
|
||||
await getTime(this.configure),
|
||||
@ -94,7 +97,7 @@ export class TokenService {
|
||||
);
|
||||
refreshToken.expiredAt = now.add(config.refreshTokenExpired, 'second').toDate();
|
||||
refreshToken.accessToken = accessToken;
|
||||
await refreshToken.save();
|
||||
await this.refreshTokenRepository.save(refreshToken);
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
@ -103,7 +106,10 @@ export class TokenService {
|
||||
* @param value
|
||||
*/
|
||||
async checkAccessToken(value: string) {
|
||||
return AccessTokenEntity.findOne({ where: { value }, relations: ['user', 'refreshToken'] });
|
||||
return this.accessTokenRepository.findOne({
|
||||
where: { value },
|
||||
relations: ['user', 'refreshToken'],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -111,9 +117,9 @@ export class TokenService {
|
||||
* @param value
|
||||
*/
|
||||
async removeAccessToken(value: string) {
|
||||
const accessToken = await AccessTokenEntity.findOne({ where: { value } });
|
||||
const accessToken = await this.accessTokenRepository.findOne({ where: { value } });
|
||||
if (accessToken) {
|
||||
await accessToken.remove();
|
||||
await this.accessTokenRepository.remove(accessToken);
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,15 +128,15 @@ export class TokenService {
|
||||
* @param value
|
||||
*/
|
||||
async removeRefreshToken(value: string) {
|
||||
const refreshToken = await RefreshTokenEntity.findOne({
|
||||
const refreshToken = await this.refreshTokenRepository.findOne({
|
||||
where: { value },
|
||||
relations: ['accessToken'],
|
||||
});
|
||||
if (refreshToken) {
|
||||
if (refreshToken.accessToken) {
|
||||
await refreshToken.accessToken.remove();
|
||||
await this.accessTokenRepository.remove(refreshToken.accessToken);
|
||||
}
|
||||
await refreshToken.remove();
|
||||
await this.refreshTokenRepository.remove(refreshToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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];
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify
|
||||
import { existsSync } from 'fs-extra';
|
||||
import { isNil } from 'lodash';
|
||||
|
||||
import { JwtAuthGuard } from '@/modules/user/guards';
|
||||
import { UserModule } from '@/modules/user/user.module';
|
||||
|
||||
import * as configs from './config';
|
||||
@ -16,6 +15,7 @@ import { CreateOptions } from './modules/core/types';
|
||||
import * as dbCommands from './modules/database/commands';
|
||||
import { DatabaseModule } from './modules/database/database.module';
|
||||
import { MeiliModule } from './modules/meilisearch/meili.module';
|
||||
import { RbacGuard } from './modules/rbac/guards/rbac.guard';
|
||||
import { Restful } from './modules/restful/restful';
|
||||
import { RestfulModule } from './modules/restful/restful.module';
|
||||
import { ApiConfig } from './modules/restful/types';
|
||||
@ -30,7 +30,7 @@ export const createOptions: CreateOptions = {
|
||||
await ContentModule.forRoot(configure),
|
||||
await UserModule.forRoot(configure),
|
||||
],
|
||||
globals: { guard: JwtAuthGuard },
|
||||
globals: { guard: RbacGuard },
|
||||
builder: async ({ configure, BootModule }) => {
|
||||
const container = await NestFactory.create<NestFastifyApplication>(
|
||||
BootModule,
|
||||
|
Loading…
Reference in New Issue
Block a user