Compare commits

...

4 Commits

Author SHA1 Message Date
9461fc53f3 add rbac module 2025-06-30 13:12:21 +08:00
fab90132b0 add rbac module 2025-06-29 23:17:15 +08:00
bff2c4a4c7 add rbac module 2025-06-29 22:07:51 +08:00
23c8eba867 add rbac module 2025-06-29 20:07:05 +08:00
33 changed files with 1294 additions and 677 deletions

View File

@ -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",

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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',
}

View File

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

View File

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

View File

@ -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);
}
}

View 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);
}
}

View 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);
}
}

View File

@ -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);

View File

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

View 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)),
],
};
}
}

View File

@ -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;
}

View File

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

View 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 };
};

View File

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

View File

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

View File

@ -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
View 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));
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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');
}
}

View File

@ -1 +1,3 @@
export * from './user.repository';
export * from './access.token.repository';
export * from './refresh.token.repository';

View 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');
}
}

View File

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

View File

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

View File

@ -1,4 +1,3 @@
/* eslint-disable import/no-extraneous-dependencies */
import { ForbiddenException, Injectable } from '@nestjs/common';
import { FastifyRequest as Request } from 'fastify';

View File

@ -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);
}
}

View File

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

View File

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

View File

@ -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,