feat: 添加登陆日志模块

master
luoer 2023-08-17 20:16:15 +08:00
parent 2877bf29fc
commit a693960017
24 changed files with 389 additions and 50 deletions

Binary file not shown.

View File

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

View File

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

View File

@ -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<any>): void | Promise<any> {
event.entity.createdBy = this.getUser();
}
beforeUpdate(event: UpdateEvent<any>): void | Promise<any> {
event.entity.updatedBy = this.getUser();
}
beforeSoftRemove(event: SoftRemoveEvent<any>): void | Promise<any> {
event.entity.deletedBy = this.getUser();
}
getUser() {
const user = EntitySubscripber.request?.user;
if (!user) {
return;
}
return `${user.nickname}(${user.id})`;
}
}

View File

@ -0,0 +1,11 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request } from 'express';
import { EntitySubscripber } from './entify.subscriber';
@Injectable()
export class RequestMiddleware<R extends Request = any, T = any> implements NestMiddleware<R, T> {
use(req: R, res: T, next: (error?: any) => void) {
EntitySubscripber.setRequest(req);
next();
}
}

View File

@ -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<LoginedUserVo> {
return this.authService.signIn(user);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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[];

View File

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

View File

@ -0,0 +1,10 @@
import { IsString } from 'class-validator';
export class CreateLogDto {
/**
* (Swagger)
* @example 'demo'
*/
@IsString()
demo: string;
}

View File

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

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateLogDto } from './create-log.dto';
export class UpdateLogDto extends PartialType(CreateLogDto) {}

View File

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

View File

@ -0,0 +1,2 @@
export class Log {}

5
src/monitor/log/index.ts Normal file
View File

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

View File

@ -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<any>): Observable<any> | Promise<Observable<any>> {
return next.handle().pipe(
tap({
next(data) {
console.log('auth ok', data);
},
error(err) {
console.log('auth err', err);
},
}),
);
}
success() {}
}

View File

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

View File

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

View File

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

1
src/types/env.d.ts vendored
View File

@ -11,6 +11,7 @@ declare module 'express' {
user?: {
id: number;
username: string;
nickname: string;
};
}
}