add rbac module
This commit is contained in:
parent
db5b553a93
commit
2c70ce4cdf
@ -1,11 +1,14 @@
|
|||||||
import { Configure } from '@/modules/config/configure';
|
import { Configure } from '@/modules/config/configure';
|
||||||
import { ConfigureFactory } from '@/modules/config/types';
|
import { ConfigureFactory } from '@/modules/config/types';
|
||||||
import * as contentControllers from '@/modules/content/controllers';
|
import { createContentApi } from '@/modules/content/routes';
|
||||||
|
import { createRbacApi } from '@/modules/rbac/routes';
|
||||||
import { ApiConfig, VersionOption } from '@/modules/restful/types';
|
import { ApiConfig, VersionOption } from '@/modules/restful/types';
|
||||||
import { createUserApi } from '@/modules/user/routes';
|
import { createUserApi } from '@/modules/user/routes';
|
||||||
|
|
||||||
export const v1 = async (configure: Configure): Promise<VersionOption> => {
|
export const v1 = async (configure: Configure): Promise<VersionOption> => {
|
||||||
|
const contentApi = createContentApi();
|
||||||
const userApi = createUserApi();
|
const userApi = createUserApi();
|
||||||
|
const rbacApi = createRbacApi();
|
||||||
return {
|
return {
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
@ -14,21 +17,26 @@ export const v1 = async (configure: Configure): Promise<VersionOption> => {
|
|||||||
controllers: [],
|
controllers: [],
|
||||||
doc: {
|
doc: {
|
||||||
description: 'app name desc',
|
description: 'app name desc',
|
||||||
|
tags: [...contentApi.tags.app, ...rbacApi.tags.app, ...userApi.tags.app],
|
||||||
|
},
|
||||||
|
children: [...contentApi.routes.app, ...rbacApi.routes.app, ...userApi.routes.app],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'manager',
|
||||||
|
path: 'manager',
|
||||||
|
controllers: [],
|
||||||
|
doc: {
|
||||||
|
description: '后台管理接口',
|
||||||
tags: [
|
tags: [
|
||||||
{ name: '分类操作', description: '对分类进行CRUD操作' },
|
...contentApi.tags.manager,
|
||||||
{ name: '标签操作', description: '对标签进行CRUD操作' },
|
...rbacApi.tags.manager,
|
||||||
{ name: '文章操作', description: '对文章进行CRUD操作' },
|
...userApi.tags.manager,
|
||||||
{ name: '评论操作', description: '对评论进行CRUD操作' },
|
|
||||||
...userApi.tags.app,
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
...contentApi.routes.manager,
|
||||||
name: 'app.content',
|
...rbacApi.routes.manager,
|
||||||
path: 'content',
|
...userApi.routes.manager,
|
||||||
controllers: Object.values(contentControllers),
|
|
||||||
},
|
|
||||||
...userApi.routes.app,
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -13,6 +13,8 @@ import { DatabaseModule } from '@/modules/database/database.module';
|
|||||||
|
|
||||||
import { addEntities, addSubscribers } from '@/modules/database/utils';
|
import { addEntities, addSubscribers } from '@/modules/database/utils';
|
||||||
|
|
||||||
|
import { UserRepository } from '@/modules/user/repositories';
|
||||||
|
|
||||||
import { Configure } from '../config/configure';
|
import { Configure } from '../config/configure';
|
||||||
|
|
||||||
import { defauleContentConfig } from './config';
|
import { defauleContentConfig } from './config';
|
||||||
@ -34,6 +36,7 @@ export class ContentModule {
|
|||||||
repositories.CategoryRepository,
|
repositories.CategoryRepository,
|
||||||
repositories.TagRepository,
|
repositories.TagRepository,
|
||||||
services.CategoryService,
|
services.CategoryService,
|
||||||
|
UserRepository,
|
||||||
{ token: services.SearchService, optional: true },
|
{ token: services.SearchService, optional: true },
|
||||||
],
|
],
|
||||||
useFactory(
|
useFactory(
|
||||||
@ -42,12 +45,14 @@ export class ContentModule {
|
|||||||
tagRepository: repositories.TagRepository,
|
tagRepository: repositories.TagRepository,
|
||||||
categoryService: services.CategoryService,
|
categoryService: services.CategoryService,
|
||||||
searchService: SearchService,
|
searchService: SearchService,
|
||||||
|
userRepository: UserRepository,
|
||||||
) {
|
) {
|
||||||
return new PostService(
|
return new PostService(
|
||||||
postRepository,
|
postRepository,
|
||||||
categoryRepository,
|
categoryRepository,
|
||||||
categoryService,
|
categoryService,
|
||||||
tagRepository,
|
tagRepository,
|
||||||
|
userRepository,
|
||||||
searchService,
|
searchService,
|
||||||
config.searchType,
|
config.searchType,
|
||||||
);
|
);
|
||||||
|
@ -1,15 +1,4 @@
|
|||||||
import {
|
import { Controller, Get, Param, ParseUUIDPipe, Query, SerializeOptions } from '@nestjs/common';
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
Delete,
|
|
||||||
Get,
|
|
||||||
Param,
|
|
||||||
ParseUUIDPipe,
|
|
||||||
Patch,
|
|
||||||
Post,
|
|
||||||
Query,
|
|
||||||
SerializeOptions,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
@ -17,11 +6,12 @@ import { Depends } from '@/modules/restful/decorators/depend.decorator';
|
|||||||
|
|
||||||
import { PaginateDto } from '@/modules/restful/dtos/paginate.dto';
|
import { PaginateDto } from '@/modules/restful/dtos/paginate.dto';
|
||||||
|
|
||||||
|
import { Guest } from '@/modules/user/decorators/guest.decorator';
|
||||||
|
|
||||||
import { ContentModule } from '../content.module';
|
import { ContentModule } from '../content.module';
|
||||||
import { CreateCategoryDto, UpdateCategoryDto } from '../dtos/category.dto';
|
|
||||||
import { CategoryService } from '../services';
|
import { CategoryService } from '../services';
|
||||||
|
|
||||||
@ApiTags('Category Operate')
|
@ApiTags('分类查询')
|
||||||
@Depends(ContentModule)
|
@Depends(ContentModule)
|
||||||
@Controller('category')
|
@Controller('category')
|
||||||
export class CategoryController {
|
export class CategoryController {
|
||||||
@ -31,6 +21,7 @@ export class CategoryController {
|
|||||||
* Search category tree
|
* Search category tree
|
||||||
*/
|
*/
|
||||||
@Get('tree')
|
@Get('tree')
|
||||||
|
@Guest()
|
||||||
@SerializeOptions({ groups: ['category-tree'] })
|
@SerializeOptions({ groups: ['category-tree'] })
|
||||||
async tree() {
|
async tree() {
|
||||||
return this.service.findTrees();
|
return this.service.findTrees();
|
||||||
@ -41,6 +32,7 @@ export class CategoryController {
|
|||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
@Get()
|
@Get()
|
||||||
|
@Guest()
|
||||||
@SerializeOptions({ groups: ['category-list'] })
|
@SerializeOptions({ groups: ['category-list'] })
|
||||||
async list(
|
async list(
|
||||||
@Query()
|
@Query()
|
||||||
@ -54,32 +46,9 @@ export class CategoryController {
|
|||||||
* @param id
|
* @param id
|
||||||
*/
|
*/
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@Guest()
|
||||||
@SerializeOptions({ groups: ['category-detail'] })
|
@SerializeOptions({ groups: ['category-detail'] })
|
||||||
async detail(@Param('id', new ParseUUIDPipe()) id: string) {
|
async detail(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||||
return this.service.detail(id);
|
return this.service.detail(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
|
||||||
@SerializeOptions({ groups: ['category-detail'] })
|
|
||||||
async store(
|
|
||||||
@Body()
|
|
||||||
data: CreateCategoryDto,
|
|
||||||
) {
|
|
||||||
return this.service.create(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch()
|
|
||||||
@SerializeOptions({ groups: ['category-detail'] })
|
|
||||||
async update(
|
|
||||||
@Body()
|
|
||||||
data: UpdateCategoryDto,
|
|
||||||
) {
|
|
||||||
return this.service.update(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete(':id')
|
|
||||||
@SerializeOptions({ groups: ['category-detail'] })
|
|
||||||
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
|
||||||
return this.service.delete([id]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,22 @@
|
|||||||
import { Body, Controller, Delete, Get, Post, Query, SerializeOptions } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Post, Query, SerializeOptions } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { In } from 'typeorm';
|
||||||
|
|
||||||
|
import { CommentEntity } from '@/modules/content/entities';
|
||||||
|
import { CommentRepository } from '@/modules/content/repositories';
|
||||||
|
import { PermissionAction } from '@/modules/rbac/constants';
|
||||||
|
import { Permission } from '@/modules/rbac/decorators/permission.decorator';
|
||||||
|
import { PermissionChecker } from '@/modules/rbac/types';
|
||||||
|
import { checkOwnerPermission } from '@/modules/rbac/utils';
|
||||||
import { Depends } from '@/modules/restful/decorators/depend.decorator';
|
import { Depends } from '@/modules/restful/decorators/depend.decorator';
|
||||||
|
|
||||||
|
import { Guest } from '@/modules/user/decorators/guest.decorator';
|
||||||
|
|
||||||
|
import { RequestUser } from '@/modules/user/decorators/user.request.decorator';
|
||||||
|
|
||||||
|
import { UserEntity } from '@/modules/user/entities';
|
||||||
|
|
||||||
import { ContentModule } from '../content.module';
|
import { ContentModule } from '../content.module';
|
||||||
import {
|
import {
|
||||||
CreateCommentDto,
|
CreateCommentDto,
|
||||||
@ -11,18 +26,41 @@ import {
|
|||||||
} from '../dtos/comment.dto';
|
} from '../dtos/comment.dto';
|
||||||
import { CommentService } from '../services';
|
import { CommentService } from '../services';
|
||||||
|
|
||||||
|
const permissions: Record<'create' | 'owner', PermissionChecker> = {
|
||||||
|
create: async (ab) => ab.can(PermissionAction.CREATE, CommentEntity.name),
|
||||||
|
owner: async (ab, ref, request) =>
|
||||||
|
checkOwnerPermission(ab, {
|
||||||
|
request,
|
||||||
|
getData: async (items) =>
|
||||||
|
ref.get(CommentRepository, { strict: false }).find({
|
||||||
|
relations: ['user'],
|
||||||
|
where: { id: In(items) },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiTags('评论操作')
|
||||||
@Depends(ContentModule)
|
@Depends(ContentModule)
|
||||||
@Controller('comment')
|
@Controller('comment')
|
||||||
export class CommentController {
|
export class CommentController {
|
||||||
constructor(protected service: CommentService) {}
|
constructor(protected service: CommentService) {}
|
||||||
|
/**
|
||||||
|
* 查询评论树
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
@Get('tree')
|
@Get('tree')
|
||||||
|
@Guest()
|
||||||
@SerializeOptions({ groups: ['comment-tree'] })
|
@SerializeOptions({ groups: ['comment-tree'] })
|
||||||
async tree(@Query() options: QueryCommentTreeDto) {
|
async tree(@Query() options: QueryCommentTreeDto) {
|
||||||
return this.service.findTrees(options);
|
return this.service.findTrees(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询评论列表
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
@Get()
|
@Get()
|
||||||
|
@Guest()
|
||||||
@SerializeOptions({ groups: ['comment-list'] })
|
@SerializeOptions({ groups: ['comment-list'] })
|
||||||
async list(
|
async list(
|
||||||
@Query()
|
@Query()
|
||||||
@ -31,13 +69,26 @@ export class CommentController {
|
|||||||
return this.service.paginate(options);
|
return this.service.paginate(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增评论
|
||||||
|
* @param data
|
||||||
|
* @param author
|
||||||
|
*/
|
||||||
@Post()
|
@Post()
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Permission(permissions.create)
|
||||||
@SerializeOptions({ groups: ['comment-detail'] })
|
@SerializeOptions({ groups: ['comment-detail'] })
|
||||||
async store(@Body() data: CreateCommentDto) {
|
async store(@Body() data: CreateCommentDto, @RequestUser() author: ClassToPlain<UserEntity>) {
|
||||||
return this.service.create(data);
|
return this.service.create(data, author);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除评论
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
@Delete()
|
@Delete()
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Permission(permissions.owner)
|
||||||
@SerializeOptions({ groups: ['comment-detail'] })
|
@SerializeOptions({ groups: ['comment-detail'] })
|
||||||
async delete(@Body() data: DeleteCommentDto) {
|
async delete(@Body() data: DeleteCommentDto) {
|
||||||
return this.service.delete(data.ids);
|
return this.service.delete(data.ids);
|
||||||
|
@ -0,0 +1,98 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
ParseUUIDPipe,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
SerializeOptions,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { ContentModule } from '@/modules/content/content.module';
|
||||||
|
import { CreateCategoryDto, UpdateCategoryDto } from '@/modules/content/dtos/category.dto';
|
||||||
|
import { CategoryEntity } from '@/modules/content/entities';
|
||||||
|
import { CategoryService } from '@/modules/content/services';
|
||||||
|
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 { PaginateDto } from '@/modules/restful/dtos/paginate.dto';
|
||||||
|
|
||||||
|
const permission: PermissionChecker = async (ab) =>
|
||||||
|
ab.can(PermissionAction.MANAGE, CategoryEntity.name);
|
||||||
|
|
||||||
|
@ApiTags('分类管理')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Depends(ContentModule)
|
||||||
|
@Controller('category')
|
||||||
|
export class CategoryController {
|
||||||
|
constructor(protected service: CategoryService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search category tree
|
||||||
|
*/
|
||||||
|
@Get('tree')
|
||||||
|
@Permission(permission)
|
||||||
|
@SerializeOptions({ groups: ['category-tree'] })
|
||||||
|
async tree() {
|
||||||
|
return this.service.findTrees();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询分类列表
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
@Get()
|
||||||
|
@Permission(permission)
|
||||||
|
@SerializeOptions({ groups: ['category-list'] })
|
||||||
|
async list(
|
||||||
|
@Query()
|
||||||
|
options: PaginateDto,
|
||||||
|
) {
|
||||||
|
return this.service.paginate(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询分类明细
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
@Get(':id')
|
||||||
|
@Permission(permission)
|
||||||
|
@SerializeOptions({ groups: ['category-detail'] })
|
||||||
|
async detail(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||||
|
return this.service.detail(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Permission(permission)
|
||||||
|
@SerializeOptions({ groups: ['category-detail'] })
|
||||||
|
async store(
|
||||||
|
@Body()
|
||||||
|
data: CreateCategoryDto,
|
||||||
|
) {
|
||||||
|
return this.service.create(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch()
|
||||||
|
@Permission(permission)
|
||||||
|
@SerializeOptions({ groups: ['category-detail'] })
|
||||||
|
async update(
|
||||||
|
@Body()
|
||||||
|
data: UpdateCategoryDto,
|
||||||
|
) {
|
||||||
|
return this.service.update(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@Permission(permission)
|
||||||
|
@SerializeOptions({ groups: ['category-detail'] })
|
||||||
|
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||||
|
return this.service.delete([id]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
import { Body, Controller, Delete, Get, Query, SerializeOptions } from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { ContentModule } from '@/modules/content/content.module';
|
||||||
|
import { QueryCommentDto } from '@/modules/content/dtos/comment.dto';
|
||||||
|
import { DeleteDto } from '@/modules/content/dtos/delete.dto';
|
||||||
|
import { CommentEntity } from '@/modules/content/entities';
|
||||||
|
import { CommentService } from '@/modules/content/services';
|
||||||
|
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';
|
||||||
|
|
||||||
|
const permission: PermissionChecker = async (ab) =>
|
||||||
|
ab.can(PermissionAction.MANAGE, CommentEntity.name);
|
||||||
|
|
||||||
|
@ApiTags('评论管理')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Depends(ContentModule)
|
||||||
|
@Controller('comments')
|
||||||
|
export class CommentController {
|
||||||
|
constructor(protected service: CommentService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询评论列表
|
||||||
|
* @param query
|
||||||
|
*/
|
||||||
|
@Get()
|
||||||
|
@SerializeOptions({ groups: ['comment-list'] })
|
||||||
|
@Permission(permission)
|
||||||
|
async list(
|
||||||
|
@Query()
|
||||||
|
query: QueryCommentDto,
|
||||||
|
) {
|
||||||
|
return this.service.paginate(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除评论
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
@Delete()
|
||||||
|
@SerializeOptions({ groups: ['comment-list'] })
|
||||||
|
@Permission(permission)
|
||||||
|
async delete(
|
||||||
|
@Body()
|
||||||
|
data: DeleteDto,
|
||||||
|
) {
|
||||||
|
const { ids } = data;
|
||||||
|
return this.service.delete(ids);
|
||||||
|
}
|
||||||
|
}
|
4
src/modules/content/controllers/manager/index.ts
Normal file
4
src/modules/content/controllers/manager/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from './category.controller';
|
||||||
|
export * from './tag.controller';
|
||||||
|
export * from './post.controller';
|
||||||
|
export * from './comment.controller';
|
130
src/modules/content/controllers/manager/post.controller.ts
Normal file
130
src/modules/content/controllers/manager/post.controller.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
ParseUUIDPipe,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
SerializeOptions,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { isNil } from 'lodash';
|
||||||
|
|
||||||
|
import { ContentModule } from '@/modules/content/content.module';
|
||||||
|
import { DeleteWithTrashDto, RestoreDto } from '@/modules/content/dtos/delete.with.trash.dto';
|
||||||
|
import { PostEntity } from '@/modules/content/entities';
|
||||||
|
import { PostService } from '@/modules/content/services/post.service';
|
||||||
|
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 { UserEntity } from '@/modules/user/entities';
|
||||||
|
|
||||||
|
import { CreatePostDto, QueryPostDto, UpdatePostDto } from '../../dtos/post.dto';
|
||||||
|
|
||||||
|
const permission: PermissionChecker = async (ab) =>
|
||||||
|
ab.can(PermissionAction.MANAGE, PostEntity.name);
|
||||||
|
|
||||||
|
@ApiTags('文章管理')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Depends(ContentModule)
|
||||||
|
@Controller('posts')
|
||||||
|
export class PostController {
|
||||||
|
constructor(protected service: PostService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询文章列表
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
@Get()
|
||||||
|
@SerializeOptions({ groups: ['post-list'] })
|
||||||
|
@Permission(permission)
|
||||||
|
async manageList(
|
||||||
|
@Query()
|
||||||
|
options: QueryPostDto,
|
||||||
|
) {
|
||||||
|
return this.service.paginate(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询文章详情
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
@Get(':id')
|
||||||
|
@SerializeOptions({ groups: ['post-detail'] })
|
||||||
|
@Permission(permission)
|
||||||
|
async manageDetail(
|
||||||
|
@Param('id', new ParseUUIDPipe())
|
||||||
|
id: string,
|
||||||
|
) {
|
||||||
|
return this.service.detail(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增文章
|
||||||
|
* @param author
|
||||||
|
* @param data
|
||||||
|
* @param user
|
||||||
|
*/
|
||||||
|
@Post()
|
||||||
|
@SerializeOptions({ groups: ['post-detail'] })
|
||||||
|
@Permission(permission)
|
||||||
|
async storeManage(
|
||||||
|
@Body()
|
||||||
|
{ author, ...data }: CreatePostDto,
|
||||||
|
@RequestUser() user: ClassToPlain<UserEntity>,
|
||||||
|
) {
|
||||||
|
return this.service.create(data, {
|
||||||
|
id: isNil(author) ? user.id : author,
|
||||||
|
} as ClassToPlain<UserEntity>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新文章
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
@Patch()
|
||||||
|
@SerializeOptions({ groups: ['post-detail'] })
|
||||||
|
@Permission(permission)
|
||||||
|
async manageUpdate(
|
||||||
|
@Body()
|
||||||
|
data: UpdatePostDto,
|
||||||
|
) {
|
||||||
|
return this.service.update(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除文章
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
@Delete()
|
||||||
|
@SerializeOptions({ groups: ['post-list'] })
|
||||||
|
@Permission(permission)
|
||||||
|
async manageDelete(
|
||||||
|
@Body()
|
||||||
|
data: DeleteWithTrashDto,
|
||||||
|
) {
|
||||||
|
const { ids, trash } = data;
|
||||||
|
return this.service.delete(ids, trash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量恢复文章
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
@Patch('restore')
|
||||||
|
@SerializeOptions({ groups: ['post-list'] })
|
||||||
|
@Permission(permission)
|
||||||
|
async manageRestore(
|
||||||
|
@Body()
|
||||||
|
data: RestoreDto,
|
||||||
|
) {
|
||||||
|
const { ids } = data;
|
||||||
|
return this.service.restore(ids);
|
||||||
|
}
|
||||||
|
}
|
99
src/modules/content/controllers/manager/tag.controller.ts
Normal file
99
src/modules/content/controllers/manager/tag.controller.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
ParseUUIDPipe,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
SerializeOptions,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { ContentModule } from '@/modules/content/content.module';
|
||||||
|
import { DeleteDto } from '@/modules/content/dtos/delete.dto';
|
||||||
|
import { CreateTagDto, UpdateTagDto } from '@/modules/content/dtos/tag.dto';
|
||||||
|
import { TagEntity } from '@/modules/content/entities';
|
||||||
|
import { TagService } from '@/modules/content/services';
|
||||||
|
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 { PaginateDto } from '@/modules/restful/dtos/paginate.dto';
|
||||||
|
|
||||||
|
const permission: PermissionChecker = async (ab) => ab.can(PermissionAction.MANAGE, TagEntity.name);
|
||||||
|
|
||||||
|
@ApiTags('标签查询')
|
||||||
|
@Depends(ContentModule)
|
||||||
|
@Controller('tag')
|
||||||
|
export class TagController {
|
||||||
|
constructor(protected service: TagService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询标签列表
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
@Get()
|
||||||
|
@Permission(permission)
|
||||||
|
@SerializeOptions({})
|
||||||
|
async list(
|
||||||
|
@Query()
|
||||||
|
options: PaginateDto,
|
||||||
|
) {
|
||||||
|
return this.service.paginate(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询标签详情
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
@Get(':id')
|
||||||
|
@Permission(permission)
|
||||||
|
@SerializeOptions({})
|
||||||
|
async detail(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||||
|
return this.service.detail(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加新标签
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
@Post()
|
||||||
|
@Permission(permission)
|
||||||
|
@SerializeOptions({})
|
||||||
|
async store(
|
||||||
|
@Body()
|
||||||
|
data: CreateTagDto,
|
||||||
|
) {
|
||||||
|
return this.service.create(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新标签
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
@Patch()
|
||||||
|
@Permission(permission)
|
||||||
|
@SerializeOptions({})
|
||||||
|
async update(
|
||||||
|
@Body()
|
||||||
|
data: UpdateTagDto,
|
||||||
|
) {
|
||||||
|
return this.service.update(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除标签
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
@Delete()
|
||||||
|
@Permission(permission)
|
||||||
|
@SerializeOptions({})
|
||||||
|
async delete(@Body() data: DeleteDto) {
|
||||||
|
return this.service.delete(data.ids);
|
||||||
|
}
|
||||||
|
}
|
@ -11,64 +11,179 @@ import {
|
|||||||
SerializeOptions,
|
SerializeOptions,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
|
||||||
import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dtos/post.dto';
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { In, IsNull, Not } from 'typeorm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FrontendCreatePostDto,
|
||||||
|
FrontendQueryPostDto,
|
||||||
|
OwnerQueryPostDto,
|
||||||
|
OwnerUpdatePostDto,
|
||||||
|
} from '@/modules/content/dtos/post.dto';
|
||||||
|
import { PostEntity } from '@/modules/content/entities';
|
||||||
|
import { PostRepository } from '@/modules/content/repositories';
|
||||||
import { PostService } from '@/modules/content/services/post.service';
|
import { PostService } from '@/modules/content/services/post.service';
|
||||||
|
|
||||||
|
import { SelectTrashMode } from '@/modules/database/constants';
|
||||||
|
import { PermissionAction } from '@/modules/rbac/constants';
|
||||||
|
import { Permission } from '@/modules/rbac/decorators/permission.decorator';
|
||||||
|
import { PermissionChecker } from '@/modules/rbac/types';
|
||||||
|
import { checkOwnerPermission } from '@/modules/rbac/utils';
|
||||||
import { Depends } from '@/modules/restful/decorators/depend.decorator';
|
import { Depends } from '@/modules/restful/decorators/depend.decorator';
|
||||||
|
|
||||||
|
import { Guest } from '@/modules/user/decorators/guest.decorator';
|
||||||
|
|
||||||
|
import { RequestUser } from '@/modules/user/decorators/user.request.decorator';
|
||||||
|
import { UserEntity } from '@/modules/user/entities';
|
||||||
|
|
||||||
import { ContentModule } from '../content.module';
|
import { ContentModule } from '../content.module';
|
||||||
import { DeleteWithTrashDto, RestoreDto } from '../dtos/delete.with.trash.dto';
|
import { DeleteWithTrashDto, RestoreDto } from '../dtos/delete.with.trash.dto';
|
||||||
|
|
||||||
|
const permissions: Record<'create' | 'owner', PermissionChecker> = {
|
||||||
|
create: async (ab) => ab.can(PermissionAction.CREATE, PostEntity.name),
|
||||||
|
owner: async (ab, ref, request) =>
|
||||||
|
checkOwnerPermission(ab, {
|
||||||
|
request,
|
||||||
|
getData: async (items) =>
|
||||||
|
ref
|
||||||
|
.get(PostRepository, { strict: false })
|
||||||
|
.find({ relations: ['author'], where: { id: In(items) } }),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiTags('文章操作')
|
||||||
@Depends(ContentModule)
|
@Depends(ContentModule)
|
||||||
@Controller('posts')
|
@Controller('posts')
|
||||||
export class PostController {
|
export class PostController {
|
||||||
constructor(private postService: PostService) {}
|
constructor(private postService: PostService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询文章列表
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
@Get()
|
@Get()
|
||||||
|
@Guest()
|
||||||
@SerializeOptions({ groups: ['post-list'] })
|
@SerializeOptions({ groups: ['post-list'] })
|
||||||
async list(
|
async list(
|
||||||
@Query()
|
@Query()
|
||||||
options: QueryPostDto,
|
options: FrontendQueryPostDto,
|
||||||
) {
|
) {
|
||||||
return this.postService.paginate(options);
|
return this.postService.paginate({
|
||||||
|
...options,
|
||||||
|
isPublished: true,
|
||||||
|
trashed: SelectTrashMode.NONE,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询自己发布的文章列表
|
||||||
|
* @param options
|
||||||
|
* @param author
|
||||||
|
*/
|
||||||
|
@Get('owner')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@SerializeOptions({ groups: ['post-list'] })
|
||||||
|
async listOwner(
|
||||||
|
@Query()
|
||||||
|
options: OwnerQueryPostDto,
|
||||||
|
@RequestUser() author: ClassToPlain<UserEntity>,
|
||||||
|
) {
|
||||||
|
return this.postService.paginate({
|
||||||
|
...options,
|
||||||
|
author: author.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询文章详情
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@Guest()
|
||||||
@SerializeOptions({ groups: ['post-detail'] })
|
@SerializeOptions({ groups: ['post-detail'] })
|
||||||
async show(@Param('id', new ParseUUIDPipe()) id: string) {
|
async show(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||||
return this.postService.detail(id);
|
return this.postService.detail(id, async (qb) =>
|
||||||
|
qb.andWhere({ publishedAt: Not(IsNull()), deletedAt: Not(IsNull()) }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
/**
|
||||||
|
* 查询自己发布的文章详情
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
@Get('owner/:id')
|
||||||
|
@ApiBearerAuth()
|
||||||
@SerializeOptions({ groups: ['post-detail'] })
|
@SerializeOptions({ groups: ['post-detail'] })
|
||||||
|
@Permission(permissions.owner)
|
||||||
|
async detailOwner(
|
||||||
|
@Param('id', new ParseUUIDPipe())
|
||||||
|
id: string,
|
||||||
|
) {
|
||||||
|
return this.postService.detail(id, async (qb) => qb.withDeleted());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增文章
|
||||||
|
* @param data
|
||||||
|
* @param author
|
||||||
|
*/
|
||||||
|
@Post()
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@SerializeOptions({ groups: ['post-detail'] })
|
||||||
|
@Permission(permissions.create)
|
||||||
async store(
|
async store(
|
||||||
@Body()
|
@Body()
|
||||||
data: CreatePostDto,
|
data: FrontendCreatePostDto,
|
||||||
|
@RequestUser() author: ClassToPlain<UserEntity>,
|
||||||
) {
|
) {
|
||||||
return this.postService.create(data);
|
return this.postService.create(data, author);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新自己发布的文章
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
@Patch()
|
@Patch()
|
||||||
|
@ApiBearerAuth()
|
||||||
@SerializeOptions({ groups: ['post-detail'] })
|
@SerializeOptions({ groups: ['post-detail'] })
|
||||||
|
@Permission(permissions.owner)
|
||||||
async update(
|
async update(
|
||||||
@Body()
|
@Body()
|
||||||
data: UpdatePostDto,
|
data: OwnerUpdatePostDto,
|
||||||
) {
|
) {
|
||||||
return this.postService.update(data);
|
return this.postService.update(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除自己发布的文章
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
@Delete()
|
@Delete()
|
||||||
@SerializeOptions({ groups: ['post-detail'] })
|
@ApiBearerAuth()
|
||||||
async delete(@Body() data: DeleteWithTrashDto) {
|
@SerializeOptions({ groups: ['post-list'] })
|
||||||
return this.postService.delete(data.ids, data.trash);
|
@Permission(permissions.owner)
|
||||||
|
async delete(
|
||||||
|
@Body()
|
||||||
|
data: DeleteWithTrashDto,
|
||||||
|
) {
|
||||||
|
const { ids, trash } = data;
|
||||||
|
return this.postService.delete(ids, trash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量恢复自己发布的文章
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
@Patch('restore')
|
@Patch('restore')
|
||||||
@SerializeOptions({ groups: ['post-detail'] })
|
@ApiBearerAuth()
|
||||||
|
@SerializeOptions({ groups: ['post-list'] })
|
||||||
|
@Permission(permissions.owner)
|
||||||
async restore(
|
async restore(
|
||||||
@Body()
|
@Body()
|
||||||
data: RestoreDto,
|
data: RestoreDto,
|
||||||
) {
|
) {
|
||||||
return this.postService.restore(data.ids);
|
const { ids } = data;
|
||||||
|
return this.postService.restore(ids);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,28 @@
|
|||||||
import {
|
import { Controller, Get, Param, ParseUUIDPipe, Query, SerializeOptions } from '@nestjs/common';
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
Delete,
|
|
||||||
Get,
|
|
||||||
Param,
|
|
||||||
ParseUUIDPipe,
|
|
||||||
Patch,
|
|
||||||
Post,
|
|
||||||
Query,
|
|
||||||
SerializeOptions,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
import { DeleteDto } from '@/modules/content/dtos/delete.dto';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
import { Depends } from '@/modules/restful/decorators/depend.decorator';
|
import { Depends } from '@/modules/restful/decorators/depend.decorator';
|
||||||
|
|
||||||
import { PaginateDto } from '@/modules/restful/dtos/paginate.dto';
|
import { PaginateDto } from '@/modules/restful/dtos/paginate.dto';
|
||||||
|
|
||||||
|
import { Guest } from '@/modules/user/decorators/guest.decorator';
|
||||||
|
|
||||||
import { ContentModule } from '../content.module';
|
import { ContentModule } from '../content.module';
|
||||||
import { CreateTagDto, UpdateTagDto } from '../dtos/tag.dto';
|
|
||||||
import { TagService } from '../services';
|
import { TagService } from '../services';
|
||||||
|
|
||||||
|
@ApiTags('标签查询')
|
||||||
@Depends(ContentModule)
|
@Depends(ContentModule)
|
||||||
@Controller('tag')
|
@Controller('tag')
|
||||||
export class TagController {
|
export class TagController {
|
||||||
constructor(protected service: TagService) {}
|
constructor(protected service: TagService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询标签列表
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
@Get()
|
@Get()
|
||||||
|
@Guest()
|
||||||
@SerializeOptions({})
|
@SerializeOptions({})
|
||||||
async list(
|
async list(
|
||||||
@Query()
|
@Query()
|
||||||
@ -35,33 +31,14 @@ export class TagController {
|
|||||||
return this.service.paginate(options);
|
return this.service.paginate(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询标签详情
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@Guest()
|
||||||
@SerializeOptions({})
|
@SerializeOptions({})
|
||||||
async detail(@Param('id', new ParseUUIDPipe()) id: string) {
|
async detail(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||||
return this.service.detail(id);
|
return this.service.detail(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
|
||||||
@SerializeOptions({})
|
|
||||||
async store(
|
|
||||||
@Body()
|
|
||||||
data: CreateTagDto,
|
|
||||||
) {
|
|
||||||
return this.service.create(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch()
|
|
||||||
@SerializeOptions({})
|
|
||||||
async update(
|
|
||||||
@Body()
|
|
||||||
date: UpdateTagDto,
|
|
||||||
) {
|
|
||||||
return this.service.update(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete()
|
|
||||||
@SerializeOptions({})
|
|
||||||
async delete(@Body() data: DeleteDto) {
|
|
||||||
return this.service.delete(data.ids);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { PartialType } from '@nestjs/swagger';
|
import { OmitType, PartialType } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -7,6 +7,7 @@ import {
|
|||||||
IsEnum,
|
IsEnum,
|
||||||
IsInt,
|
IsInt,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsUUID,
|
IsUUID,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
@ -23,6 +24,8 @@ import { SelectTrashMode } from '@/modules/database/constants';
|
|||||||
import { IsDataExist } from '@/modules/database/constraints/data.exist.constraint';
|
import { IsDataExist } from '@/modules/database/constraints/data.exist.constraint';
|
||||||
import { PaginateOptions } from '@/modules/database/types';
|
import { PaginateOptions } from '@/modules/database/types';
|
||||||
|
|
||||||
|
import { UserEntity } from '@/modules/user/entities';
|
||||||
|
|
||||||
import { CategoryEntity, PostEntity, TagEntity } from '../entities';
|
import { CategoryEntity, PostEntity, TagEntity } from '../entities';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,10 +93,23 @@ export class QueryPostDto implements PaginateOptions {
|
|||||||
@IsUUID(undefined, { message: 'The ID format is incorrect' })
|
@IsUUID(undefined, { message: 'The ID format is incorrect' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
tag?: string;
|
tag?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据文章作者ID查询
|
||||||
|
*/
|
||||||
|
@IsDataExist(UserEntity, {
|
||||||
|
message: '指定的用户不存在',
|
||||||
|
})
|
||||||
|
@IsUUID(undefined, { message: '用户ID格式错误' })
|
||||||
|
@IsOptional()
|
||||||
|
author?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@DtoValidation({ groups: ['create'] })
|
@DtoValidation({ groups: ['create'] })
|
||||||
export class CreatePostDto {
|
export class CreatePostDto {
|
||||||
|
/**
|
||||||
|
* 文章标题
|
||||||
|
*/
|
||||||
@MaxLength(255, {
|
@MaxLength(255, {
|
||||||
always: true,
|
always: true,
|
||||||
message: 'The maximum length of the article title is $constraint1',
|
message: 'The maximum length of the article title is $constraint1',
|
||||||
@ -102,10 +118,16 @@ export class CreatePostDto {
|
|||||||
@IsOptional({ groups: ['update'] })
|
@IsOptional({ groups: ['update'] })
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章内容
|
||||||
|
*/
|
||||||
@IsNotEmpty({ groups: ['create'], message: 'The content of the article must be filled in.' })
|
@IsNotEmpty({ groups: ['create'], message: 'The content of the article must be filled in.' })
|
||||||
@IsOptional({ groups: ['update'] })
|
@IsOptional({ groups: ['update'] })
|
||||||
body: string;
|
body: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章描述
|
||||||
|
*/
|
||||||
@MaxLength(500, {
|
@MaxLength(500, {
|
||||||
always: true,
|
always: true,
|
||||||
message: 'The maximum length of the article description is $constraint1',
|
message: 'The maximum length of the article description is $constraint1',
|
||||||
@ -113,12 +135,18 @@ export class CreatePostDto {
|
|||||||
@IsOptional({ always: true })
|
@IsOptional({ always: true })
|
||||||
summary?: string;
|
summary?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否发布(发布时间)
|
||||||
|
*/
|
||||||
@Transform(({ value }) => toBoolean(value))
|
@Transform(({ value }) => toBoolean(value))
|
||||||
@IsBoolean({ always: true })
|
@IsBoolean({ always: true })
|
||||||
@ValidateIf((value) => !isNil(value.publish))
|
@ValidateIf((value) => !isNil(value.publish))
|
||||||
@IsOptional({ always: true })
|
@IsOptional({ always: true })
|
||||||
publish?: boolean;
|
publish?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SEO关键字
|
||||||
|
*/
|
||||||
@MaxLength(20, {
|
@MaxLength(20, {
|
||||||
always: true,
|
always: true,
|
||||||
each: true,
|
each: true,
|
||||||
@ -127,12 +155,18 @@ export class CreatePostDto {
|
|||||||
@IsOptional({ always: true })
|
@IsOptional({ always: true })
|
||||||
keywords?: string[];
|
keywords?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义排序
|
||||||
|
*/
|
||||||
@Transform(({ value }) => toNumber(value))
|
@Transform(({ value }) => toNumber(value))
|
||||||
@Min(0, { message: 'The sorted value must be greater than 0.', always: true })
|
@Min(0, { message: 'The sorted value must be greater than 0.', always: true })
|
||||||
@IsInt({ always: true })
|
@IsInt({ always: true })
|
||||||
@IsOptional({ always: true })
|
@IsOptional({ always: true })
|
||||||
customOrder?: number;
|
customOrder?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所属分类ID
|
||||||
|
*/
|
||||||
@IsDataExist(CategoryEntity, { always: true, message: 'The category does not exist' })
|
@IsDataExist(CategoryEntity, { always: true, message: 'The category does not exist' })
|
||||||
@IsUUID(undefined, {
|
@IsUUID(undefined, {
|
||||||
always: true,
|
always: true,
|
||||||
@ -141,6 +175,9 @@ export class CreatePostDto {
|
|||||||
@IsOptional({ always: true })
|
@IsOptional({ always: true })
|
||||||
category?: string;
|
category?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联标签ID
|
||||||
|
*/
|
||||||
@IsDataExist(TagEntity, {
|
@IsDataExist(TagEntity, {
|
||||||
always: true,
|
always: true,
|
||||||
each: true,
|
each: true,
|
||||||
@ -153,10 +190,30 @@ export class CreatePostDto {
|
|||||||
})
|
})
|
||||||
@IsOptional({ always: true })
|
@IsOptional({ always: true })
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章作者ID:可用于在管理员发布文章时分配给其它用户,如果不设置,则作者为当前管理员
|
||||||
|
*/
|
||||||
|
@IsDataExist(UserEntity, {
|
||||||
|
always: true,
|
||||||
|
message: '用户不存在',
|
||||||
|
})
|
||||||
|
@IsUUID(undefined, {
|
||||||
|
always: true,
|
||||||
|
message: '用户ID格式不正确',
|
||||||
|
})
|
||||||
|
@IsOptional({ always: true })
|
||||||
|
author?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章更新验证
|
||||||
|
*/
|
||||||
@DtoValidation({ groups: ['update'] })
|
@DtoValidation({ groups: ['update'] })
|
||||||
export class UpdatePostDto extends PartialType(CreatePostDto) {
|
export class UpdatePostDto extends PartialType(CreatePostDto) {
|
||||||
|
/**
|
||||||
|
* 待更新ID
|
||||||
|
*/
|
||||||
@IsUUID(undefined, {
|
@IsUUID(undefined, {
|
||||||
groups: ['update'],
|
groups: ['update'],
|
||||||
message: 'The format of the article ID is incorrect.',
|
message: 'The format of the article ID is incorrect.',
|
||||||
@ -165,3 +222,45 @@ export class UpdatePostDto extends PartialType(CreatePostDto) {
|
|||||||
@IsDataExist(PostEntity, { groups: ['update'], message: 'post id not exist when update' })
|
@IsDataExist(PostEntity, { groups: ['update'], message: 'post id not exist when update' })
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端查询文章列表验证
|
||||||
|
*/
|
||||||
|
@DtoValidation({ type: 'query' })
|
||||||
|
export class FrontendQueryPostDto extends OmitType(QueryPostDto, ['isPublished', 'trashed']) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端创建文章验证
|
||||||
|
*/
|
||||||
|
@DtoValidation({ groups: ['create'] })
|
||||||
|
export class FrontendCreatePostDto extends OmitType(CreatePostDto, ['author', 'customOrder']) {
|
||||||
|
/**
|
||||||
|
* 用户侧排序:文章在用户的文章管理而非后台中,列表的排序规则
|
||||||
|
*/
|
||||||
|
@Transform(({ value }) => toNumber(value))
|
||||||
|
@Min(0, { always: true, message: '排序值必须大于0' })
|
||||||
|
@IsNumber(undefined, { always: true })
|
||||||
|
@IsOptional({ always: true })
|
||||||
|
userOrder?: number = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户文章更新验证
|
||||||
|
*/
|
||||||
|
@DtoValidation({ groups: ['update'] })
|
||||||
|
export class OwnerUpdatePostDto extends OmitType(UpdatePostDto, ['author', 'customOrder']) {
|
||||||
|
/**
|
||||||
|
* 用户侧排序:文章在用户的文章管理而非后台中,列表的排序规则
|
||||||
|
*/
|
||||||
|
@Transform(({ value }) => toNumber(value))
|
||||||
|
@Min(0, { always: true, message: '排序值必须大于0' })
|
||||||
|
@IsNumber(undefined, { always: true })
|
||||||
|
@IsOptional({ always: true })
|
||||||
|
userOrder?: number = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户查询自己的文章列表验证
|
||||||
|
*/
|
||||||
|
@DtoValidation({ type: 'query' })
|
||||||
|
export class OwnerQueryPostDto extends OmitType(QueryPostDto, ['author']) {}
|
||||||
|
44
src/modules/content/routes.ts
Normal file
44
src/modules/content/routes.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { RouteOption, TagOption } from '@/modules/restful/types';
|
||||||
|
|
||||||
|
import * as controllers from './controllers';
|
||||||
|
import * as manageControllers from './controllers/manager';
|
||||||
|
|
||||||
|
export const createContentApi = () => {
|
||||||
|
const routes: Record<'app' | 'manager', RouteOption[]> = {
|
||||||
|
app: [
|
||||||
|
{
|
||||||
|
name: 'app.content',
|
||||||
|
path: 'content',
|
||||||
|
controllers: Object.values(controllers),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
manager: [
|
||||||
|
{
|
||||||
|
name: 'manage.content',
|
||||||
|
path: 'content',
|
||||||
|
controllers: Object.values(manageControllers),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const tags: Record<'app' | 'manager', Array<string | TagOption>> = {
|
||||||
|
app: [
|
||||||
|
{ name: '分类查询', description: '查询分类信息' },
|
||||||
|
{ name: '标签查询', description: '查询标签信息' },
|
||||||
|
{
|
||||||
|
name: '文章操作',
|
||||||
|
description: '查询文章以及对自己的文章进行CRUD操作',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '评论操作',
|
||||||
|
description: '查看评论以及对自己的评论进行CRD操作',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
manager: [
|
||||||
|
{ name: '分类管理', description: '管理分类信息' },
|
||||||
|
{ name: '标签管理', description: '管理标签信息' },
|
||||||
|
{ name: '文章管理', description: '管理文章信息' },
|
||||||
|
{ name: '评论管理', description: '管理评论信息' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
return { routes, tags };
|
||||||
|
};
|
@ -13,11 +13,14 @@ import { CommentEntity } from '@/modules/content/entities/comment.entity';
|
|||||||
import { CommentRepository, PostRepository } from '@/modules/content/repositories';
|
import { CommentRepository, PostRepository } from '@/modules/content/repositories';
|
||||||
import { BaseService } from '@/modules/database/base/service';
|
import { BaseService } from '@/modules/database/base/service';
|
||||||
import { treePaginate } from '@/modules/database/utils';
|
import { treePaginate } from '@/modules/database/utils';
|
||||||
|
import { UserEntity } from '@/modules/user/entities';
|
||||||
|
import { UserRepository } from '@/modules/user/repositories';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommentService extends BaseService<CommentEntity, CommentRepository> {
|
export class CommentService extends BaseService<CommentEntity, CommentRepository> {
|
||||||
constructor(
|
constructor(
|
||||||
protected repository: CommentRepository,
|
protected repository: CommentRepository,
|
||||||
|
protected userRepository: UserRepository,
|
||||||
protected postRepository: PostRepository,
|
protected postRepository: PostRepository,
|
||||||
) {
|
) {
|
||||||
super(repository);
|
super(repository);
|
||||||
@ -50,7 +53,7 @@ export class CommentService extends BaseService<CommentEntity, CommentRepository
|
|||||||
return treePaginate(query, comments);
|
return treePaginate(query, comments);
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(data: CreateCommentDto) {
|
async create(data: CreateCommentDto, author: ClassToPlain<UserEntity>) {
|
||||||
const parent = await this.getParent(undefined, data.parent);
|
const parent = await this.getParent(undefined, data.parent);
|
||||||
if (!isNil(parent) && parent.post.id !== data.post) {
|
if (!isNil(parent) && parent.post.id !== data.post) {
|
||||||
throw new ForbiddenException('Parent comment and child comment must belong same post!');
|
throw new ForbiddenException('Parent comment and child comment must belong same post!');
|
||||||
@ -59,6 +62,7 @@ export class CommentService extends BaseService<CommentEntity, CommentRepository
|
|||||||
...data,
|
...data,
|
||||||
parent,
|
parent,
|
||||||
post: await this.getPost(data.post),
|
post: await this.getPost(data.post),
|
||||||
|
author: await this.userRepository.findOneByOrFail({ id: author.id }),
|
||||||
});
|
});
|
||||||
return this.repository.findOneOrFail({
|
return this.repository.findOneOrFail({
|
||||||
where: { id: item.id },
|
where: { id: item.id },
|
||||||
|
@ -5,7 +5,12 @@ import { isArray, isFunction, omit, pick } from 'lodash';
|
|||||||
import { EntityNotFoundError, In, IsNull, Not, SelectQueryBuilder } from 'typeorm';
|
import { EntityNotFoundError, In, IsNull, Not, SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
import { PostOrder } from '@/modules/content/constants';
|
import { PostOrder } from '@/modules/content/constants';
|
||||||
import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dtos/post.dto';
|
import {
|
||||||
|
CreatePostDto,
|
||||||
|
FrontendCreatePostDto,
|
||||||
|
QueryPostDto,
|
||||||
|
UpdatePostDto,
|
||||||
|
} from '@/modules/content/dtos/post.dto';
|
||||||
import { PostEntity } from '@/modules/content/entities/post.entity';
|
import { PostEntity } from '@/modules/content/entities/post.entity';
|
||||||
import { CategoryRepository } from '@/modules/content/repositories';
|
import { CategoryRepository } from '@/modules/content/repositories';
|
||||||
import { PostRepository } from '@/modules/content/repositories/post.repository';
|
import { PostRepository } from '@/modules/content/repositories/post.repository';
|
||||||
@ -16,6 +21,10 @@ import { SelectTrashMode } from '@/modules/database/constants';
|
|||||||
import { QueryHook } from '@/modules/database/types';
|
import { QueryHook } from '@/modules/database/types';
|
||||||
import { paginate } from '@/modules/database/utils';
|
import { paginate } from '@/modules/database/utils';
|
||||||
|
|
||||||
|
import { UserEntity } from '@/modules/user/entities';
|
||||||
|
|
||||||
|
import { UserRepository } from '@/modules/user/repositories';
|
||||||
|
|
||||||
import { TagRepository } from '../repositories/tag.repository';
|
import { TagRepository } from '../repositories/tag.repository';
|
||||||
|
|
||||||
import { CategoryService } from './category.service';
|
import { CategoryService } from './category.service';
|
||||||
@ -33,6 +42,7 @@ export class PostService extends BaseService<PostEntity, PostRepository, FindPar
|
|||||||
protected categoryRepository: CategoryRepository,
|
protected categoryRepository: CategoryRepository,
|
||||||
protected categoryService: CategoryService,
|
protected categoryService: CategoryService,
|
||||||
protected tagRepository: TagRepository,
|
protected tagRepository: TagRepository,
|
||||||
|
protected userRepository: UserRepository,
|
||||||
protected searchService?: SearchService,
|
protected searchService?: SearchService,
|
||||||
protected searchType: SearchType = 'mysql',
|
protected searchType: SearchType = 'mysql',
|
||||||
) {
|
) {
|
||||||
@ -61,11 +71,19 @@ export class PostService extends BaseService<PostEntity, PostRepository, FindPar
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(data: CreatePostDto) {
|
/**
|
||||||
|
* 创建文章
|
||||||
|
* @param data
|
||||||
|
* @param author
|
||||||
|
*/
|
||||||
|
async create(data: CreatePostDto | FrontendCreatePostDto, author: ClassToPlain<UserEntity>) {
|
||||||
let publishedAt: Date | null;
|
let publishedAt: Date | null;
|
||||||
if (!isNil(data.publish)) {
|
if (!isNil(data.publish)) {
|
||||||
publishedAt = data.publish ? new Date() : null;
|
publishedAt = data.publish ? new Date() : null;
|
||||||
}
|
}
|
||||||
|
const authorId = isNil((data as CreatePostDto).author)
|
||||||
|
? author.id
|
||||||
|
: (data as CreatePostDto).author;
|
||||||
const createPostDto = {
|
const createPostDto = {
|
||||||
...omit(data, ['publish']),
|
...omit(data, ['publish']),
|
||||||
category: isNil(data.category)
|
category: isNil(data.category)
|
||||||
@ -73,6 +91,7 @@ export class PostService extends BaseService<PostEntity, PostRepository, FindPar
|
|||||||
: await this.categoryRepository.findOneOrFail({ where: { id: data.category } }),
|
: await this.categoryRepository.findOneOrFail({ where: { id: data.category } }),
|
||||||
tags: isArray(data.tags) ? await this.tagRepository.findBy({ id: In(data.tags) }) : [],
|
tags: isArray(data.tags) ? await this.tagRepository.findBy({ id: In(data.tags) }) : [],
|
||||||
publishedAt,
|
publishedAt,
|
||||||
|
author: await this.userRepository.findOneByOrFail({ id: authorId }),
|
||||||
};
|
};
|
||||||
const item = await this.repository.save(createPostDto);
|
const item = await this.repository.save(createPostDto);
|
||||||
const result = await this.detail(item.id);
|
const result = await this.detail(item.id);
|
||||||
@ -82,12 +101,20 @@ export class PostService extends BaseService<PostEntity, PostRepository, FindPar
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新文章
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
async update(data: UpdatePostDto) {
|
async update(data: UpdatePostDto) {
|
||||||
let publishedAt: Date | null;
|
let publishedAt: Date | null;
|
||||||
if (!isNil(data.publish)) {
|
if (!isNil(data.publish)) {
|
||||||
publishedAt = data.publish ? new Date() : null;
|
publishedAt = data.publish ? new Date() : null;
|
||||||
}
|
}
|
||||||
const post = await this.detail(data.id);
|
const post = await this.detail(data.id);
|
||||||
|
if (!isNil(data.author)) {
|
||||||
|
post.author = await this.userRepository.findOneByOrFail({ id: data.author });
|
||||||
|
await this.repository.save(post);
|
||||||
|
}
|
||||||
if (data.category !== undefined) {
|
if (data.category !== undefined) {
|
||||||
post.category = isNil(data.category)
|
post.category = isNil(data.category)
|
||||||
? null
|
? null
|
||||||
@ -102,7 +129,7 @@ export class PostService extends BaseService<PostEntity, PostRepository, FindPar
|
|||||||
.addAndRemove(data.tags, post.tags ?? []);
|
.addAndRemove(data.tags, post.tags ?? []);
|
||||||
}
|
}
|
||||||
await this.repository.update(data.id, {
|
await this.repository.update(data.id, {
|
||||||
...omit(data, ['id', 'publish', 'tags', 'category']),
|
...omit(data, ['id', 'publish', 'tags', 'category', 'author']),
|
||||||
publishedAt,
|
publishedAt,
|
||||||
});
|
});
|
||||||
const result = await this.detail(data.id);
|
const result = await this.detail(data.id);
|
||||||
|
@ -4,7 +4,7 @@ import * as controllers from './controllers';
|
|||||||
import * as manageControllers from './controllers/manager';
|
import * as manageControllers from './controllers/manager';
|
||||||
|
|
||||||
export const createRbacApi = () => {
|
export const createRbacApi = () => {
|
||||||
const routes: Record<'app' | 'manage', RouteOption[]> = {
|
const routes: Record<'app' | 'manager', RouteOption[]> = {
|
||||||
app: [
|
app: [
|
||||||
{
|
{
|
||||||
name: 'app.rbac',
|
name: 'app.rbac',
|
||||||
@ -12,7 +12,7 @@ export const createRbacApi = () => {
|
|||||||
controllers: Object.values(controllers),
|
controllers: Object.values(controllers),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
manage: [
|
manager: [
|
||||||
{
|
{
|
||||||
name: 'manage.rbac',
|
name: 'manage.rbac',
|
||||||
path: 'rbac',
|
path: 'rbac',
|
||||||
@ -20,9 +20,9 @@ export const createRbacApi = () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const tags: Record<'app' | 'manage', Array<string | TagOption>> = {
|
const tags: Record<'app' | 'manager', Array<string | TagOption>> = {
|
||||||
app: [{ name: '角色查询', description: '查询角色信息' }],
|
app: [{ name: '角色查询', description: '查询角色信息' }],
|
||||||
manage: [
|
manager: [
|
||||||
{ name: '角色管理', description: '管理角色信息' },
|
{ name: '角色管理', description: '管理角色信息' },
|
||||||
{ name: '权限信息', description: '查询权限信息' },
|
{ name: '权限信息', description: '查询权限信息' },
|
||||||
],
|
],
|
||||||
|
@ -16,7 +16,7 @@ export async function checkOwnerPermission<T extends ObjectLiteral>(
|
|||||||
getData: (items: string[]) => Promise<T[]>;
|
getData: (items: string[]) => Promise<T[]>;
|
||||||
permission?: string;
|
permission?: string;
|
||||||
},
|
},
|
||||||
) {
|
): Promise<boolean> {
|
||||||
const { request, key, getData, permission } = options;
|
const { request, key, getData, permission } = options;
|
||||||
const models = await getData(getRequestData(request, key));
|
const models = await getData(getRequestData(request, key));
|
||||||
return models.every((model) => ability.can(permission ?? PermissionAction.OWNER, model));
|
return models.every((model) => ability.can(permission ?? PermissionAction.OWNER, model));
|
||||||
|
Loading…
Reference in New Issue
Block a user