diff --git a/content/database/db.sqlite b/content/database/db.sqlite index b9145fb..596a431 100644 Binary files a/content/database/db.sqlite and b/content/database/db.sqlite differ diff --git a/src/database/database.module.ts b/src/database/database.module.ts index 4a4f65b..01bc2f4 100644 --- a/src/database/database.module.ts +++ b/src/database/database.module.ts @@ -1,24 +1,37 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigService } from '@/config'; import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; +import { EntitySubscripber } from './suscribers/entify.subscriber'; +import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; +import { RequestMiddleware } from './suscribers/request.middleware'; /** * 数据库模块 * @description 基于 `typeorm` 封装 */ -export const DatabaseModule = TypeOrmModule.forRootAsync({ - useFactory: (config: ConfigService) => { - if (config.dbType === 'sqlite') { - return { - type: config.dbType, - database: config.dbSqlitePath, - synchronize: true, - autoLoadEntities: true, - namingStrategy: new SnakeNamingStrategy(), - }; - } - if (config.dbType === 'mysql') { - } - }, - inject: [ConfigService], -}); +@Module({ + imports: [ + TypeOrmModule.forRootAsync({ + useFactory: (config: ConfigService) => { + if (config.dbType === 'sqlite') { + return { + type: config.dbType, + database: config.dbSqlitePath, + synchronize: true, + autoLoadEntities: true, + namingStrategy: new SnakeNamingStrategy(), + }; + } + if (config.dbType === 'mysql') { + } + }, + inject: [ConfigService], + }), + ], + providers: [EntitySubscripber], +}) +export class DatabaseModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(RequestMiddleware).forRoutes('*'); + } +} diff --git a/src/database/entities/base.ts b/src/database/entities/base.ts index d7cc7c3..f632dd1 100644 --- a/src/database/entities/base.ts +++ b/src/database/entities/base.ts @@ -13,42 +13,48 @@ export class BaseEntity { */ @PrimaryGeneratedColumn({ comment: '自增ID' }) id: number; + /** * 创建时间 * @example "2022-01-01 10:10:10" */ @CreateDateColumn({ comment: '创建时间' }) createdAt: Date; + /** - * 创建人ID - * @example 1 + * 创建人 + * @example '绝弹(1)' */ @Column({ comment: '创建人', nullable: true }) - createdBy: number; + createdBy: string; + /** * 更新时间 * @example "2022-01-02 11:11:11" */ @UpdateDateColumn({ comment: '更新时间' }) updatedAt: Date; + /** - * 更新人ID - * @example 1 + * 更新人 + * @example '绝弹(1)' */ @Column({ comment: '更新人', nullable: true }) - updatedBy: number; + updatedBy: string; + /** * 删除时间 * @example "2022-01-03 12:12:12" */ @Exclude() - @DeleteDateColumn({ comment: '删除时间' }) + @DeleteDateColumn({ comment: '删除时间', select: false }) deleteddAt: Date; + /** * 删除人ID * @example 1 */ @Exclude() - @Column({ comment: '删除人', nullable: true }) - deletedBy: number; + @Column({ comment: '删除人', nullable: true, select: false }) + deletedBy: string; } diff --git a/src/database/suscribers/entify.subscriber.ts b/src/database/suscribers/entify.subscriber.ts new file mode 100644 index 0000000..63c9803 --- /dev/null +++ b/src/database/suscribers/entify.subscriber.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; +import { Request } from 'express'; +import { EntitySubscriberInterface, InsertEvent, DataSource, UpdateEvent, SoftRemoveEvent } from 'typeorm'; + +/** + * 实体订阅器 + * @description 自动插入创建/更新用户的ID + */ +@Injectable() +export class EntitySubscripber implements EntitySubscriberInterface { + static request: Request; + + constructor(private datasource: DataSource) { + this.datasource.subscribers.push(this); + } + + static setRequest(req: Request) { + this.request = req; + } + + beforeInsert(event: InsertEvent): void | Promise { + event.entity.createdBy = this.getUser(); + } + + beforeUpdate(event: UpdateEvent): void | Promise { + event.entity.updatedBy = this.getUser(); + } + + beforeSoftRemove(event: SoftRemoveEvent): void | Promise { + event.entity.deletedBy = this.getUser(); + } + + getUser() { + const user = EntitySubscripber.request?.user; + if (!user) { + return; + } + return `${user.nickname}(${user.id})`; + } +} diff --git a/src/database/suscribers/request.middleware.ts b/src/database/suscribers/request.middleware.ts new file mode 100644 index 0000000..7b08b70 --- /dev/null +++ b/src/database/suscribers/request.middleware.ts @@ -0,0 +1,11 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request } from 'express'; +import { EntitySubscripber } from './entify.subscriber'; + +@Injectable() +export class RequestMiddleware implements NestMiddleware { + use(req: R, res: T, next: (error?: any) => void) { + EntitySubscripber.setRequest(req); + next(); + } +} diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index a392ba8..cd8f3a4 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,9 +1,10 @@ -import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { Body, Controller, HttpCode, HttpStatus, Post, UseInterceptors } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AuthService } from './auth.service'; import { AuthUserDto } from './dto/auth-user.dto'; import { Public } from './jwt'; import { LoginedUserVo } from './vo/logined-user.vo'; +import { AuthLogInterceptor } from '@/monitor/log'; @ApiTags('auth') @Controller('auth') @@ -16,6 +17,7 @@ export class AuthController { @Post('login') @Public() @HttpCode(HttpStatus.OK) + @UseInterceptors(AuthLogInterceptor) login(@Body() user: AuthUserDto): Promise { return this.authService.signIn(user); } diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index a83c445..5150ee3 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -17,7 +17,8 @@ export class AuthService { throw new UnauthorizedException('密码错误'); } const loginedUser = Object.assign(new LoginedUserVo(), user); - loginedUser.token = await this.jwtService.signAsync({ id: user.id, username: user.username }); + const { id, username, nickname } = loginedUser; + loginedUser.token = await this.jwtService.signAsync({ id, username, nickname }); return loginedUser; } } diff --git a/src/modules/role/dto/update-role.dto.ts b/src/modules/role/dto/update-role.dto.ts index 450134d..8db879c 100644 --- a/src/modules/role/dto/update-role.dto.ts +++ b/src/modules/role/dto/update-role.dto.ts @@ -1,4 +1,9 @@ import { PartialType } from '@nestjs/swagger'; import { CreateRoleDto } from './create-role.dto'; +import { IsInt, IsOptional } from 'class-validator'; -export class UpdateRoleDto extends PartialType(CreateRoleDto) {} +export class UpdateRoleDto extends PartialType(CreateRoleDto) { + @IsOptional() + @IsInt({ each: true }) + permissionIds?: number[]; +} diff --git a/src/modules/role/entities/role.entity.ts b/src/modules/role/entities/role.entity.ts index 44ed513..19b86ad 100644 --- a/src/modules/role/entities/role.entity.ts +++ b/src/modules/role/entities/role.entity.ts @@ -1,7 +1,7 @@ import { BaseEntity } from '@/database'; import { Permission } from '@/modules/permission/entities/permission.entity'; import { User } from 'src/modules/user'; -import { Column, Entity, JoinTable, ManyToMany } from 'typeorm'; +import { Column, Entity, JoinTable, ManyToMany, RelationId } from 'typeorm'; @Entity() export class Role extends BaseEntity { @@ -11,18 +11,21 @@ export class Role extends BaseEntity { */ @Column({ comment: '角色名称' }) name: string; + /** * 角色标识 * @example 'admin' */ @Column({ comment: '角色标识' }) slug: string; + /** * 角色描述 * @example '拥有所有权限' */ @Column({ comment: '角色描述', nullable: true }) description: string; + /** * 角色权限 * @example [1, 2, 3] @@ -30,6 +33,14 @@ export class Role extends BaseEntity { @JoinTable() @ManyToMany(() => Permission, (permission) => permission.roles) permissions: Permission[]; + + /** + * 角色权限ID + * @example [1, 2, 3] + */ + @RelationId('permissions') + permissionIds: number[]; + /** * 角色用户 * @example [1, 2, 3] diff --git a/src/modules/role/role.service.ts b/src/modules/role/role.service.ts index ffab6ae..a140a74 100644 --- a/src/modules/role/role.service.ts +++ b/src/modules/role/role.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { CreateRoleDto } from './dto/create-role.dto'; @@ -26,7 +26,16 @@ export class RoleService { return `This action returns a #${id} role`; } - update(id: number, updateRoleDto: UpdateRoleDto) { + async update(id: number, updateRoleDto: UpdateRoleDto) { + const role = this.roleRepository.findOne({ where: { id } }); + if (!role) { + throw new NotFoundException('角色不存在'); + } + if (updateRoleDto.permissionIds) { + const permissions = updateRoleDto.permissionIds.map((id) => ({ id })); + await this.roleRepository.save({ id, permissions }); + delete updateRoleDto.permissionIds; + } return this.roleRepository.update(id, updateRoleDto); } diff --git a/src/modules/user/dto/create-user.dto.ts b/src/modules/user/dto/create-user.dto.ts index 14fb0a3..fc6f7d3 100644 --- a/src/modules/user/dto/create-user.dto.ts +++ b/src/modules/user/dto/create-user.dto.ts @@ -1,4 +1,3 @@ -import { Role } from '@/modules/role/entities/role.entity'; import { IsInt, IsOptional, IsString } from 'class-validator'; export class CreateUserDto { @@ -8,12 +7,6 @@ export class CreateUserDto { */ @IsString() username: string; - /** - * 用户密码 - * @example 'password' - */ - @IsString() - password: string; /** * 用户昵称 * @example '绝弹' @@ -21,17 +14,24 @@ export class CreateUserDto { @IsString() nickname: string; /** - * 用户头像 - * @example './assets/222421415123.png ' + * 用户密码 + * @example 'password' */ @IsOptional() @IsString() - avatar: string; + password?: string; /** - * 用户角色 + * 头像ID + * @example 1 + */ + @IsOptional() + @IsString() + avatarId?: number; + /** + * 角色ID列表 * @example [1, 2, 3] */ @IsOptional() @IsInt({ each: true }) - roles: Role[]; + roleIds?: number[]; } diff --git a/src/modules/user/entities/user.entity.ts b/src/modules/user/entities/user.entity.ts index 6e02f7a..1268278 100644 --- a/src/modules/user/entities/user.entity.ts +++ b/src/modules/user/entities/user.entity.ts @@ -62,7 +62,7 @@ export class User extends BaseEntity { * 用户角色 */ @ApiHideProperty() - @ManyToMany(() => Role, (role) => role.user) + @ManyToMany(() => Role, (role) => role.user, { cascade: true }) @JoinTable() roles: Role[]; diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index aea8299..bd34241 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -1,7 +1,7 @@ import { BaseService } from '@/common/base'; -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Like, Repository } from 'typeorm'; +import { In, Like, Repository } from 'typeorm'; import { CreateUserDto } from './dto/create-user.dto'; import { FindUserDto } from './dto/find-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; @@ -18,8 +18,9 @@ export class UserService extends BaseService { */ async create(createUserDto: CreateUserDto) { const user = this.userRepository.create(createUserDto); - if (createUserDto.roles) { - user.roles = createUserDto.roles.map((id) => ({ id })) as any; + if (createUserDto.roleIds) { + user.roles = createUserDto.roleIds.map((id) => ({ id })) as any; + delete createUserDto.roleIds; } await this.userRepository.save(user); return user.id; @@ -29,7 +30,7 @@ export class UserService extends BaseService { * 查找所有用户 */ async findMany(findUserdto: FindUserDto) { - const { nickname: _nickname, } = findUserdto; + const { nickname: _nickname } = findUserdto; const nickname = _nickname && Like(`%${_nickname}%`); const { skip, take } = this.paginizate(findUserdto, { full: true }); return this.userRepository.findAndCount({ skip, take, where: { nickname } }); @@ -46,7 +47,16 @@ export class UserService extends BaseService { /** * 根据用户id */ - update(id: number, updateUserDto: UpdateUserDto) { + async update(id: number, updateUserDto: UpdateUserDto) { + const user = await this.userRepository.findOne({ where: { id } }); + if (!user) { + throw new NotFoundException('用户不存在'); + } + if (updateUserDto.roleIds) { + const roles = updateUserDto.roleIds.map((id) => ({ id })); + await this.userRepository.save({ id, roles }); + delete updateUserDto.roleIds; + } return this.userRepository.update(id, updateUserDto); } diff --git a/src/monitor/log/dto/create-log.dto.ts b/src/monitor/log/dto/create-log.dto.ts new file mode 100644 index 0000000..1935c5e --- /dev/null +++ b/src/monitor/log/dto/create-log.dto.ts @@ -0,0 +1,10 @@ +import { IsString } from 'class-validator'; + +export class CreateLogDto { + /** + * 字段描述(Swagger用途) + * @example 'demo' + */ + @IsString() + demo: string; +} diff --git a/src/monitor/log/dto/find-log.dto.ts b/src/monitor/log/dto/find-log.dto.ts new file mode 100644 index 0000000..d4690da --- /dev/null +++ b/src/monitor/log/dto/find-log.dto.ts @@ -0,0 +1,13 @@ +import { PaginationDto } from '@/common/response'; +import { IntersectionType } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; + +export class FindLogDto extends IntersectionType(PaginationDto) { + /** + * 字段描述(Swagger用途) + * @example '示例值' + */ + @IsOptional() + @IsString() + demo?: string; +} diff --git a/src/monitor/log/dto/update-log.dto.ts b/src/monitor/log/dto/update-log.dto.ts new file mode 100644 index 0000000..99da378 --- /dev/null +++ b/src/monitor/log/dto/update-log.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateLogDto } from './create-log.dto'; + +export class UpdateLogDto extends PartialType(CreateLogDto) {} diff --git a/src/monitor/log/entities/authLog.entity.ts b/src/monitor/log/entities/authLog.entity.ts new file mode 100644 index 0000000..a53626a --- /dev/null +++ b/src/monitor/log/entities/authLog.entity.ts @@ -0,0 +1,47 @@ +import { BaseEntity } from '@/database'; +import { Column, Entity } from 'typeorm'; + +@Entity({ orderBy: { id: 'DESC' } }) +export class AuthLog extends BaseEntity { + /** + * 用户昵称 + * @example '绝弹' + */ + @Column() + nickname: string; + + /** + * 操作描述 + * @example 1 + */ + @Column() + description: string; + + /** + * 登陆IP + * @example '127.0.0.1' + */ + @Column() + ip: string; + + /** + * 登陆地址 + * @example '广东省深圳市' + */ + @Column() + addr: string; + + /** + * 浏览器 + * @example 'chrome' + */ + @Column() + browser: string; + + /** + * 操作系统 + * @example 'windows 10' + */ + @Column() + os: string; +} diff --git a/src/monitor/log/entities/log.entity.ts b/src/monitor/log/entities/log.entity.ts new file mode 100644 index 0000000..dde6161 --- /dev/null +++ b/src/monitor/log/entities/log.entity.ts @@ -0,0 +1,2 @@ + +export class Log {} \ No newline at end of file diff --git a/src/monitor/log/index.ts b/src/monitor/log/index.ts new file mode 100644 index 0000000..3988558 --- /dev/null +++ b/src/monitor/log/index.ts @@ -0,0 +1,5 @@ +export * from './entities/authLog.entity'; +export * from './log.controller'; +export * from './log.module'; +export * from './log.service'; +export * from './interceptors/authLog.interceptor'; diff --git a/src/monitor/log/interceptors/authLog.interceptor.ts b/src/monitor/log/interceptors/authLog.interceptor.ts new file mode 100644 index 0000000..b048cdd --- /dev/null +++ b/src/monitor/log/interceptors/authLog.interceptor.ts @@ -0,0 +1,22 @@ +import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'; +import { Observable, tap } from 'rxjs'; +import { LogService } from '../log.service'; + +export class AuthLogInterceptor implements NestInterceptor { + constructor(private logger: LogService) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable | Promise> { + return next.handle().pipe( + tap({ + next(data) { + console.log('auth ok', data); + }, + error(err) { + console.log('auth err', err); + }, + }), + ); + } + + success() {} +} diff --git a/src/monitor/log/log.controller.ts b/src/monitor/log/log.controller.ts new file mode 100644 index 0000000..19be047 --- /dev/null +++ b/src/monitor/log/log.controller.ts @@ -0,0 +1,59 @@ +import { BaseController } from '@/common/base'; +import { Respond, RespondType } from '@/common/response'; +import { Body, Controller, Delete, Get, Param, Patch, Post, Query, ParseIntPipe } from '@nestjs/common'; +import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { CreateLogDto } from './dto/create-log.dto'; +import { FindLogDto } from './dto/find-log.dto'; +import { UpdateLogDto } from './dto/update-log.dto'; +import { AuthLog } from './entities/authLog.entity'; +import { LogService } from './log.service'; + +@ApiTags('log') +@Controller('logs') +export class LogController extends BaseController { + constructor(private logService: LogService) { + super(); + } + + /** + * 新增日志管理 + */ + @Post() + addLog(@Body() createLogDto: CreateLogDto) { + return this.logService.create(createLogDto); + } + + /** + * 根据分页/过滤参数查询日志管理 + */ + @Get() + @Respond(RespondType.PAGINATION) + @ApiOkResponse({ isArray: true, type: AuthLog }) + getLogs(@Query() query: FindLogDto) { + return this.logService.findMany(query); + } + + /** + * 根据ID查询日志管理 + */ + @Get(':id') + getLog(@Param('id', ParseIntPipe) id: number): Promise { + return this.logService.findOne(id); + } + + /** + * 根据ID更新日志管理 + */ + @Patch(':id') + updateLog(@Param('id', ParseIntPipe) id: number, @Body() updateLogDto: UpdateLogDto) { + return this.logService.update(+id, updateLogDto); + } + + /** + * 根据ID删除日志管理 + */ + @Delete(':id') + delLog(@Param('id', ParseIntPipe) id: number) { + return this.logService.remove(+id); + } +} diff --git a/src/monitor/log/log.module.ts b/src/monitor/log/log.module.ts new file mode 100644 index 0000000..253471f --- /dev/null +++ b/src/monitor/log/log.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuthLog } from './entities/authLog.entity'; +import { LogController } from './log.controller'; +import { LogService } from './log.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([AuthLog])], + controllers: [LogController], + providers: [LogService], + exports: [LogService], +}) +export class LogModule {} diff --git a/src/monitor/log/log.service.ts b/src/monitor/log/log.service.ts new file mode 100644 index 0000000..1df8081 --- /dev/null +++ b/src/monitor/log/log.service.ts @@ -0,0 +1,55 @@ +import { BaseService } from '@/common/base'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Like, Repository } from 'typeorm'; +import { CreateLogDto } from './dto/create-log.dto'; +import { FindLogDto } from './dto/find-log.dto'; +import { UpdateLogDto } from './dto/update-log.dto'; +import { AuthLog } from './entities/authLog.entity'; + +@Injectable() +export class LogService extends BaseService { + constructor(@InjectRepository(AuthLog) private logRepository: Repository) { + super(); + } + + /** + * 新增日志管理 + */ + async create(createLogDto: CreateLogDto) { + const log = this.logRepository.create(); + await this.logRepository.save(log); + return log.id; + } + + /** + * 条件/分页查询 + */ + async findMany(findLogdto: FindLogDto) { + const { page, size } = findLogdto; + const { skip, take } = this.formatPagination(page, size, true); + return this.logRepository.findAndCount({ skip, take }); + } + + /** + * 根据ID查询 + */ + findOne(idOrOptions: number | Partial) { + const where = typeof idOrOptions === 'number' ? { id: idOrOptions } : (idOrOptions as any); + return this.logRepository.findOne({ where }); + } + + /** + * 根据ID更新 + */ + update(id: number, updateLogDto: UpdateLogDto) { + // return this.logRepository.update(); + } + + /** + * 根据ID删除(软删除) + */ + remove(id: number) { + return this.logRepository.softDelete(id); + } +} diff --git a/src/types/env.d.ts b/src/types/env.d.ts index 68adb6f..42ffe1e 100644 --- a/src/types/env.d.ts +++ b/src/types/env.d.ts @@ -11,6 +11,7 @@ declare module 'express' { user?: { id: number; username: string; + nickname: string; }; } }