diff --git a/src/config/database.config.ts b/src/config/database.config.ts index 73d2e76..b5b0625 100644 --- a/src/config/database.config.ts +++ b/src/config/database.config.ts @@ -8,10 +8,7 @@ export const database = createDBConfig((configure) => ({ common: { synchronize: true, // 启用详细日志以便调试 SQL 错误 - logging: - configure.env.get('NODE_ENV') === 'development' - ? ['query', 'error', 'schema', 'warn', 'info', 'log'] - : ['error'], + logging: configure.env.get('NODE_ENV') === 'development' ? ['error', 'query'] : ['error'], // 启用最大日志记录 maxQueryExecutionTime: 1000, }, diff --git a/src/config/meili.config.ts b/src/config/meili.config.ts index e9421f2..c3edc74 100644 --- a/src/config/meili.config.ts +++ b/src/config/meili.config.ts @@ -1,4 +1,4 @@ -import { createMeiliConfig } from '../modules/meilisearch/config'; +import { createMeiliConfig } from '@/modules/meilisearch/config'; export const meili = createMeiliConfig((configure) => [ { diff --git a/src/modules/content/content.module.ts b/src/modules/content/content.module.ts index 5431587..caf3696 100644 --- a/src/modules/content/content.module.ts +++ b/src/modules/content/content.module.ts @@ -44,8 +44,8 @@ export class ContentModule { categoryRepository: repositories.CategoryRepository, tagRepository: repositories.TagRepository, categoryService: services.CategoryService, - searchService: SearchService, userRepository: UserRepository, + searchService: SearchService, ) { return new PostService( postRepository, diff --git a/src/modules/content/controllers/post.controller.ts b/src/modules/content/controllers/post.controller.ts index 3c121ff..bff90b6 100644 --- a/src/modules/content/controllers/post.controller.ts +++ b/src/modules/content/controllers/post.controller.ts @@ -104,7 +104,7 @@ export class PostController { @SerializeOptions({ groups: ['post-detail'] }) async show(@Param('id', new ParseUUIDPipe()) id: string) { return this.postService.detail(id, async (qb) => - qb.andWhere({ publishedAt: Not(IsNull()), deletedAt: Not(IsNull()) }), + qb.andWhere({ publishedAt: Not(IsNull()), deletedAt: IsNull() }), ); } diff --git a/src/modules/content/dtos/delete.dto.ts b/src/modules/content/dtos/delete.dto.ts index efce31e..23a6e2e 100644 --- a/src/modules/content/dtos/delete.dto.ts +++ b/src/modules/content/dtos/delete.dto.ts @@ -1,4 +1,4 @@ -import { IsDefined, IsUUID } from 'class-validator'; +import { ArrayMinSize, IsArray, IsDefined, IsUUID } from 'class-validator'; import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator'; @@ -6,5 +6,7 @@ import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator export class DeleteDto { @IsUUID(undefined, { each: true, always: true, message: 'The ID format is incorrect' }) @IsDefined({ each: true, message: 'The ID must be specified' }) + @IsArray() + @ArrayMinSize(1) ids: string[]; } diff --git a/src/modules/content/entities/post.entity.ts b/src/modules/content/entities/post.entity.ts index 98d6581..61b5a95 100644 --- a/src/modules/content/entities/post.entity.ts +++ b/src/modules/content/entities/post.entity.ts @@ -39,7 +39,6 @@ export class PostEntity extends BaseEntity { @Column({ comment: '文章描述', nullable: true }) summary?: string; - @Expose() @Expose() @Column({ comment: '关键字', type: 'simple-array', nullable: true }) keywords?: string[]; @@ -69,7 +68,7 @@ export class PostEntity extends BaseEntity { @Expose() @Type(() => Date) @DeleteDateColumn({ comment: '删除时间' }) - deleteAt: Date; + deletedAt: Date; @Expose() commentCount: number; diff --git a/src/modules/content/rbac.ts b/src/modules/content/rbac.ts index d32a9a9..a14c20b 100644 --- a/src/modules/content/rbac.ts +++ b/src/modules/content/rbac.ts @@ -3,7 +3,9 @@ import { ModuleRef } from '@nestjs/core'; import { CategoryEntity, CommentEntity, PostEntity, TagEntity } from '@/modules/content/entities'; import { PermissionAction, SystemRoles } from '@/modules/rbac/constants'; +import { PermissionEntity, RoleEntity } from '@/modules/rbac/entities'; import { RbacResolver } from '@/modules/rbac/rbac.resolver'; +import { UserEntity } from '@/modules/user/entities'; @Injectable() export class ContentRbac implements OnModuleInit { @@ -73,6 +75,27 @@ export class ContentRbac implements OnModuleInit { subject: CommentEntity, }, }, + { + name: 'permission.manage', + rule: { + action: PermissionAction.MANAGE, + subject: PermissionEntity, + }, + }, + { + name: 'role.manage', + rule: { + action: PermissionAction.MANAGE, + subject: RoleEntity, + }, + }, + { + name: 'user.manage', + rule: { + action: PermissionAction.MANAGE, + subject: UserEntity, + }, + }, ]); resolver.addRoles([ diff --git a/src/modules/content/repositories/post.repository.ts b/src/modules/content/repositories/post.repository.ts index 41c42f6..d49fdf5 100644 --- a/src/modules/content/repositories/post.repository.ts +++ b/src/modules/content/repositories/post.repository.ts @@ -11,12 +11,13 @@ export class PostRepository extends BaseRepository { return this.createQueryBuilder(this.qbName) .leftJoinAndSelect(`${this.qbName}.category`, 'category') .leftJoinAndSelect(`${this.qbName}.tags`, 'tags') + .leftJoinAndSelect(`${this.qbName}.author`, 'author') .addSelect((query) => { return query .select('COUNT(c.id)', 'count') .from(CommentEntity, 'c') .where(`c.post.id = ${this.qbName}.id`); }, 'commentCount') - .loadRelationCountAndMap(`${this.qbName}.commentCOunt`, `${this.qbName}.comments`); + .loadRelationCountAndMap(`${this.qbName}.commentCount`, `${this.qbName}.comments`); } } diff --git a/src/modules/content/services/post.service.ts b/src/modules/content/services/post.service.ts index a866399..4562608 100644 --- a/src/modules/content/services/post.service.ts +++ b/src/modules/content/services/post.service.ts @@ -147,8 +147,8 @@ export class PostService extends BaseService !isNil(item.deleteAt)); - const softs = items.filter((item) => isNil(item.deleteAt)); + const directs = items.filter((item) => !isNil(item.deletedAt)); + const softs = items.filter((item) => isNil(item.deletedAt)); result = [ ...(await this.repository.remove(directs)), ...(await this.repository.softRemove(softs)), @@ -172,7 +172,7 @@ export class PostService extends BaseService !isNil(item.deleteAt)); + const trashes = items.filter((item) => !isNil(item.deletedAt)); await this.searchService.update(trashes); const trashedIds = trashes.map((item) => item.id); if (trashedIds.length < 1) { @@ -190,7 +190,7 @@ export class PostService extends BaseService, ) { - const { orderBy, isPublished, category, tag, trashed, search } = options; + const { orderBy, isPublished, category, tag, trashed, search, author } = options; if (typeof isPublished === 'boolean') { isPublished ? qb.where({ publishedAt: Not(IsNull()) }) @@ -212,6 +212,9 @@ export class PostService extends BaseService item.id)]; - return qb.where('categoryRepository.id IN (:...ids)', { ids }); + return qb.where('category.id IN (:...ids)', { ids }); } } diff --git a/src/modules/content/services/search.service.ts b/src/modules/content/services/search.service.ts index 7385c0c..41c8268 100644 --- a/src/modules/content/services/search.service.ts +++ b/src/modules/content/services/search.service.ts @@ -48,7 +48,7 @@ export class SearchService implements OnModuleInit { } async search(text: string, param: SearchOption = {}): Promise { - const option = { page: 1, limit: 10, trashed: SelectTrashMode.ONLY, ...param }; + const option = { page: 1, limit: 10, trashed: SelectTrashMode.NONE, ...param }; const limit = isNil(option.limit) || option.limit < 1 ? 1 : option.limit; const page = isNil(option.page) || option.page < 1 ? 1 : option.page; let filter = ['deletedAt IS NULL']; @@ -64,7 +64,7 @@ export class SearchService implements OnModuleInit { .index(this.index) .search(text, { page, limit, sort: ['updatedAt:desc', 'commentCount:desc'], filter }); return { - item: result.hits, + items: result.hits, currentPage: result.page, perPage: result.hitsPerPage, totalItems: result.estimatedTotalHits, diff --git a/src/modules/content/utils.ts b/src/modules/content/utils.ts index b589bd0..b1dbf0e 100644 --- a/src/modules/content/utils.ts +++ b/src/modules/content/utils.ts @@ -29,7 +29,7 @@ export async function getSearchItem( 'body', 'summary', 'commentCount', - 'deleteAt', + 'deletedAt', 'publishedAt', 'createdAt', 'updatedAt', diff --git a/src/modules/database/base/service.ts b/src/modules/database/base/service.ts index d299033..e832eee 100644 --- a/src/modules/database/base/service.ts +++ b/src/modules/database/base/service.ts @@ -83,7 +83,7 @@ export abstract class BaseService< async detail(id: string, callback?: QueryHook) { const qb = await this.buildItemQB(id, this.repository.buildBaseQB(), callback); - const item = qb.getOne(); + const item = await qb.getOne(); if (!item) { throw new NotFoundException(`${this.repository.qbName} ${id} NOT FOUND`); } diff --git a/src/modules/database/constraints/unique.constraint.ts b/src/modules/database/constraints/unique.constraint.ts index 34eb3f7..a62433e 100644 --- a/src/modules/database/constraints/unique.constraint.ts +++ b/src/modules/database/constraints/unique.constraint.ts @@ -20,6 +20,9 @@ export class UniqueConstraint implements ValidatorConstraintInterface { constructor(private dataSource: DataSource) {} async validate(value: any, validationArguments?: ValidationArguments): Promise { + if (isNil(value)) { + return true; + } const config: Omit = { property: validationArguments.property }; const condition = ('entity' in validationArguments.constraints[0] ? merge(config, validationArguments.constraints[0]) diff --git a/src/modules/database/factories/content.factory.ts b/src/modules/database/factories/content.factory.ts index f723f36..465c613 100644 --- a/src/modules/database/factories/content.factory.ts +++ b/src/modules/database/factories/content.factory.ts @@ -31,7 +31,7 @@ export const ContentFactory = defineFactory( post.publishedAt = (await getTime(configure)).toDate(); } if (Math.random() > 0.5) { - post.deleteAt = (await getTime(configure)).toDate(); + post.deletedAt = (await getTime(configure)).toDate(); } if (category) { post.category = category; diff --git a/src/modules/rbac/controllers/manager/permission.controller.ts b/src/modules/rbac/controllers/manager/permission.controller.ts index d1437a0..7b2d667 100644 --- a/src/modules/rbac/controllers/manager/permission.controller.ts +++ b/src/modules/rbac/controllers/manager/permission.controller.ts @@ -20,9 +20,6 @@ const permission: PermissionChecker = async (ab) => export class PermissionController { constructor(private service: PermissionService) {} - permission: PermissionChecker = async (ab) => - ab.can(PermissionAction.MANAGE, PermissionEntity.name); - /** * 分页列表查询 * @param options diff --git a/src/modules/rbac/entities/role.entity.ts b/src/modules/rbac/entities/role.entity.ts index 9fb9ccb..28b5678 100644 --- a/src/modules/rbac/entities/role.entity.ts +++ b/src/modules/rbac/entities/role.entity.ts @@ -30,18 +30,21 @@ export class RoleEntity extends BaseEntity { /** * 角色名称 */ + @Expose() @Column({ comment: '角色名称' }) name: string; /** * 显示名称 */ + @Expose() @Column({ comment: '显示名称', nullable: true }) label?: string; /** * 角色描述 */ + @Expose({ groups: ['role-detail'] }) @Column({ comment: '角色描述', nullable: true, type: 'text' }) description?: string; diff --git a/src/modules/rbac/guards/rbac.guard.ts b/src/modules/rbac/guards/rbac.guard.ts index 87c2fa9..5137808 100644 --- a/src/modules/rbac/guards/rbac.guard.ts +++ b/src/modules/rbac/guards/rbac.guard.ts @@ -1,27 +1,28 @@ import { createMongoAbility } from '@casl/ability'; -import { ExecutionContext, ForbiddenException } from '@nestjs/common'; +import { ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common'; import { ModuleRef, Reflector } from '@nestjs/core'; import { isNil } from 'lodash'; +import { PermissionEntity } from '@/modules/rbac/entities'; 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'; +@Injectable() export class RbacGuard extends JwtAuthGuard { constructor( protected reflector: Reflector, protected resolver: RbacResolver, protected tokenService: TokenService, protected userRepository: UserRepository, - protected modeleRef: ModuleRef, + protected moduleRef: ModuleRef, ) { super(reflector, tokenService); } @@ -45,7 +46,7 @@ export class RbacGuard extends JwtAuthGuard { resolver: this.resolver, repository: this.userRepository, checkers, - moduleRef: this.modeleRef, + moduleRef: this.moduleRef, request: context.switchToHttp().getRequest(), }); if (!result) { diff --git a/src/modules/rbac/rbac.resolver.ts b/src/modules/rbac/rbac.resolver.ts index 704fb33..4c22a02 100644 --- a/src/modules/rbac/rbac.resolver.ts +++ b/src/modules/rbac/rbac.resolver.ts @@ -205,9 +205,12 @@ export class RbacResolver

name === role.name).permissions), - }); + const rolePermissions = + isNil(role.permissions) || role.permissions.length <= 0 + ? [] + : await manager.findBy(PermissionEntity, { + name: In(role.permissions.map(({ name }) => name)), + }); await roleRepo .createQueryBuilder('role') .relation(RoleEntity, 'permissions') diff --git a/src/modules/user/controllers/manager/user.controller.ts b/src/modules/user/controllers/manager/user.controller.ts index 0735b14..10e0fe4 100644 --- a/src/modules/user/controllers/manager/user.controller.ts +++ b/src/modules/user/controllers/manager/user.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Delete, + ForbiddenException, Get, Param, ParseUUIDPipe, @@ -18,6 +19,7 @@ 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 { RequestUser } from '@/modules/user/decorators/user.request.decorator'; import { CreateUserDto, FrontedQueryUserDto, UpdateUserDto } from '@/modules/user/dtos/user.dto'; import { UserEntity } from '@/modules/user/entities'; import { UserService } from '@/modules/user/services'; @@ -40,7 +42,7 @@ export class UserController { @Permission(permission) @SerializeOptions({ groups: ['user-list'] }) async list(@Query() options: FrontedQueryUserDto) { - return this.service.list(options); + return this.service.paginate(options); } /** @@ -78,13 +80,17 @@ export class UserController { /** * 批量删除用户 + * @param user * @param data */ @Delete() @Permission(permission) @SerializeOptions({ groups: ['user-list'] }) - async delete(@Body() data: DeleteWithTrashDto) { + async delete(@RequestUser() user: UserEntity, @Body() data: DeleteWithTrashDto) { const { ids, trash } = data; + if (ids.includes(user.id)) { + throw new ForbiddenException(); + } return this.service.delete(ids, trash); } diff --git a/src/modules/user/controllers/user.controller.ts b/src/modules/user/controllers/user.controller.ts index 5b04dfe..15c3f0f 100644 --- a/src/modules/user/controllers/user.controller.ts +++ b/src/modules/user/controllers/user.controller.ts @@ -2,7 +2,7 @@ import { Controller, Get, Param, ParseUUIDPipe, Query, SerializeOptions } from ' import { ApiTags } from '@nestjs/swagger'; -import { IsNull, Not } from 'typeorm'; +import { IsNull } from 'typeorm'; import { SelectTrashMode } from '@/modules/database/constants'; import { Depends } from '@/modules/restful/decorators/depend.decorator'; @@ -25,7 +25,7 @@ export class UserController { @Guest() @SerializeOptions({ groups: ['user-list'] }) async list(@Query() options: FrontedQueryUserDto) { - return this.service.list({ ...options, trashed: SelectTrashMode.NONE }); + return this.service.paginate({ ...options, trashed: SelectTrashMode.NONE }); } /** @@ -36,6 +36,6 @@ export class UserController { @Guest() @SerializeOptions({ groups: ['user-detail'] }) async detail(@Param('id', new ParseUUIDPipe()) id: string) { - return this.service.detail(id, async (qb) => qb.andWhere({ deletedAt: Not(IsNull()) })); + return this.service.detail(id, async (qb) => qb.andWhere({ deletedAt: IsNull() })); } } diff --git a/src/modules/user/dtos/account.dto.ts b/src/modules/user/dtos/account.dto.ts index 518720a..380e017 100644 --- a/src/modules/user/dtos/account.dto.ts +++ b/src/modules/user/dtos/account.dto.ts @@ -18,7 +18,7 @@ export class UpdateAccountDto extends PickType(UserCommonDto, ['username', 'nick */ @IsUUID(undefined, { message: '用户ID格式不正确', groups: [UserValidateGroup.USER_UPDATE] }) @IsDefined({ groups: ['update'], message: '用户ID必须指定' }) - id: string; + id?: string; } /** diff --git a/src/modules/user/dtos/user.common.dto.ts b/src/modules/user/dtos/user.common.dto.ts index 4c3750e..689ea3d 100644 --- a/src/modules/user/dtos/user.common.dto.ts +++ b/src/modules/user/dtos/user.common.dto.ts @@ -38,7 +38,7 @@ export class UserCommonDto { { entity: UserEntity, ignore: 'id', ignoreKey: 'userId' }, { groups: [UserValidateGroup.ACCOUNT_UPDATE], message: '该用户名已被注册' }, ) - @Length(4, 50, { always: true, message: '用户名长度必须为$constraint1到$constraint2' }) + @Length(4, 30, { always: true, message: '用户名长度必须为$constraint1到$constraint2' }) @IsOptional({ groups: [UserValidateGroup.USER_UPDATE, UserValidateGroup.ACCOUNT_UPDATE] }) username: string; diff --git a/src/modules/user/dtos/user.dto.ts b/src/modules/user/dtos/user.dto.ts index eafce0a..ae07035 100644 --- a/src/modules/user/dtos/user.dto.ts +++ b/src/modules/user/dtos/user.dto.ts @@ -94,6 +94,7 @@ export class QueryUserDto extends PaginateWithTrashedDto { * 排序规则:可指定用户列表的排序规则,默认为按创建时间降序排序 */ @IsEnum(UserOrderType) + @IsOptional() orderBy?: UserOrderType; } diff --git a/src/modules/user/entities/user.entity.ts b/src/modules/user/entities/user.entity.ts index 136ab93..1609a93 100644 --- a/src/modules/user/entities/user.entity.ts +++ b/src/modules/user/entities/user.entity.ts @@ -109,13 +109,16 @@ export class UserEntity { * 用户权限 */ @Expose() - @ManyToMany(() => PermissionEntity, (permission) => permission.users, { cascade: true }) + @ManyToMany(() => PermissionEntity, (permission) => permission.users, { + cascade: true, + onDelete: 'CASCADE', + }) permissions: Relation[]; /** * 用户角色 */ @Expose() - @ManyToMany(() => RoleEntity, (role) => role.users, { cascade: true }) + @ManyToMany(() => RoleEntity, (role) => role.users, { cascade: true, onDelete: 'CASCADE' }) roles: Relation[]; }