feat: 添加登陆日志功能

master
luoer 2023-09-12 17:26:14 +08:00
parent a693960017
commit 868769880e
19 changed files with 277 additions and 2286 deletions

Binary file not shown.

View File

@ -30,12 +30,15 @@
"endOfLine": "auto"
},
"dependencies": {
"@nestjs/axios": "^3.0.0",
"@nestjs/common": "^9.0.0",
"@nestjs/core": "^9.0.0",
"@nestjs/platform-express": "^9.0.0",
"axios": "^1.5.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0"
"rxjs": "^7.2.0",
"ua-parser-js": "^1.0.36"
},
"devDependencies": {
"@nestjs/cache-manager": "^2.1.0",

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ 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';
import { LoginLogInterceptor } from '@/monitor/log';
@ApiTags('auth')
@Controller('auth')
@ -17,7 +17,7 @@ export class AuthController {
@Post('login')
@Public()
@HttpCode(HttpStatus.OK)
@UseInterceptors(AuthLogInterceptor)
@UseInterceptors(LoginLogInterceptor)
login(@Body() user: AuthUserDto): Promise<LoginedUserVo> {
return this.authService.signIn(user);
}

View File

@ -4,10 +4,11 @@ import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtGuard, JwtModule } from './jwt';
import { APP_GUARD } from '@nestjs/core';
import { LogModule } from '@/monitor/log';
@Module({
imports: [UserModule, JwtModule, LogModule],
controllers: [AuthController],
imports: [UserModule, JwtModule],
providers: [
AuthService,
{

View File

@ -9,14 +9,15 @@ export class AuthService {
constructor(private userService: UserService, private jwtService: JwtService) {}
async signIn(authUserDto: AuthUserDto) {
const { password, ...user } = await this.userService.findByUsername(authUserDto.username);
const user = await this.userService.findByUsername(authUserDto.username);
if (!user) {
throw new UnauthorizedException('用户名不存在');
}
if (password !== authUserDto.password) {
if (user.password !== authUserDto.password) {
throw new UnauthorizedException('密码错误');
}
const loginedUser = Object.assign(new LoginedUserVo(), user);
const { password, ...rest } = user;
const loginedUser = Object.assign(new LoginedUserVo(), rest);
const { id, username, nickname } = loginedUser;
loginedUser.token = await this.jwtService.signAsync({ id, username, nickname });
return loginedUser;

View File

@ -2,6 +2,11 @@ import { BaseEntity } from '@/database';
import { Role } from '@/modules/role/entities/role.entity';
import { Column, Entity, ManyToMany } from 'typeorm';
enum PermissionType {
Menu = 'menu',
Api = 'api',
}
@Entity()
export class Permission extends BaseEntity {
/**
@ -10,21 +15,31 @@ export class Permission extends BaseEntity {
*/
@Column({ comment: '权限名称' })
name: string;
/**
*
* @example 'post:list'
*/
@Column({ comment: '权限标识' })
slug: string;
/**
*
* @example 'menu'
*/
@Column({ nullable: true })
type: PermissionType;
/**
*
* @example '文章列表'
*/
@Column({ comment: '权限描述', nullable: true })
description: string;
/**
*
* @example '文章列表'
* @example {}
*/
@ManyToMany(() => Role, (role) => role.permissions)
roles: Role[];

View File

@ -4,6 +4,7 @@ import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreatePermissionDto } from './dto/create-permission.dto';
import { UpdatePermissionDto } from './dto/update-permission.dto';
import { PermissionService } from './permission.service';
import { PermissionWith } from './permission.decorator';
@ApiTags('permission')
@Controller('permissions')
@ -11,6 +12,7 @@ export class PermissionController {
constructor(private readonly permissionService: PermissionService) {}
@Post()
@PermissionWith('permission:add')
@ApiOperation({ description: '创建权限', operationId: 'addPermission' })
create(@Body() createPermissionDto: CreatePermissionDto) {
return this.permissionService.create(createPermissionDto);
@ -30,7 +32,7 @@ export class PermissionController {
}
@Patch(':id')
@ApiOperation({ description: '更新权限', operationId: 'updatePermission' })
@ApiOperation({ description: '更新权限', operationId: 'setPermission' })
update(@Param('id') id: string, @Body() updatePermissionDto: UpdatePermissionDto) {
return this.permissionService.update(+id, updatePermissionDto);
}

View File

@ -1,6 +1,6 @@
import { SetMetadata } from '@nestjs/common';
export const PERMISSION_KEY = 'permission';
export const PERMISSION_KEY = 'APP:PERMISSION';
/**
*
@ -25,10 +25,10 @@ export const enum PermissionEnum {
}
/**
*
*
* @param permissions
* @returns
*/
export function NeedPermission(...permissions: PermissionEnum[]) {
export function PermissionWith(...permissions: string[]) {
return SetMetadata(PERMISSION_KEY, permissions);
}

View File

@ -1,13 +1,15 @@
import { PaginationDto } from '@/common/response';
import { IntersectionType } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsOptional, IsString } from 'class-validator';
export class FindLogDto extends IntersectionType(PaginationDto) {
/**
* (Swagger)
* @example '示例值'
*
* @example '绝弹'
*/
@IsOptional()
@IsString()
demo?: string;
@Transform(({ value }) => value && value.trim())
nickname?: string;
}

View File

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

View File

@ -0,0 +1,54 @@
import { BaseEntity } from '@/database';
import { Column, Entity } from 'typeorm';
@Entity({ orderBy: { id: 'DESC' } })
export class LoginLog extends BaseEntity {
/**
*
* @example '绝弹'
*/
@Column()
nickname: string;
/**
*
* @example 1
*/
@Column()
description: string;
/**
*
* @example true
*/
@Column({ nullable: true })
status: boolean;
/**
* 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

@ -2,7 +2,7 @@ import { BaseEntity } from '@/database';
import { Column, Entity } from 'typeorm';
@Entity({ orderBy: { id: 'DESC' } })
export class AuthLog extends BaseEntity {
export class OperationLog extends BaseEntity {
/**
*
* @example '绝弹'

View File

@ -1,5 +1,5 @@
export * from './entities/authLog.entity';
export * from './entities/loginLog.entity';
export * from './log.controller';
export * from './log.module';
export * from './log.service';
export * from './interceptors/authLog.interceptor';
export * from './interceptors/loginLog.interceptor';

View File

@ -1,22 +0,0 @@
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,41 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
import { LogService } from '../log.service';
@Injectable()
export class LoginLogInterceptor implements NestInterceptor {
constructor(private logService: LogService) {}
intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> | Promise<Observable<any>> {
const _this = this;
return next.handle().pipe(
tap({
next(data) {
const status = true;
const description = '登录成功';
_this.recordLoginLog(context, { status, description });
},
error(err) {
const status = false;
const description = err?.message || '登录失败';
_this.recordLoginLog(context, { status, description });
},
}),
);
}
recordLoginLog(context: ExecutionContext, data: { status: boolean; description: string }) {
const { ip, body, headers } = context.switchToHttp().getRequest();
const { status, description } = data;
const userAgent = headers['user-agent'] as string;
const forwradIp = headers['x-forwarded-for'] as string;
const nickname = body.username;
this.logService.addLoginLog({
nickname,
status,
description,
userAgent,
ip: forwradIp || ip,
});
}
}

View File

@ -5,7 +5,7 @@ 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 { LoginLog } from './entities/loginLog.entity';
import { LogService } from './log.service';
@ApiTags('log')
@ -28,16 +28,23 @@ export class LogController extends BaseController {
*/
@Get()
@Respond(RespondType.PAGINATION)
@ApiOkResponse({ isArray: true, type: AuthLog })
@ApiOkResponse({ isArray: true, type: LoginLog })
getLogs(@Query() query: FindLogDto) {
return this.logService.findMany(query);
}
@Get('login')
@Respond(RespondType.PAGINATION)
@ApiOkResponse({ isArray: true, type: LoginLog })
getLoginLogs(@Query() query: FindLogDto) {
return this.logService.findMany(query);
}
/**
* ID
*/
@Get(':id')
getLog(@Param('id', ParseIntPipe) id: number): Promise<AuthLog> {
getLog(@Param('id', ParseIntPipe) id: number): Promise<LoginLog> {
return this.logService.findOne(id);
}

View File

@ -1,11 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthLog } from './entities/authLog.entity';
import { LoginLog } from './entities/loginLog.entity';
import { LogController } from './log.controller';
import { LogService } from './log.service';
@Module({
imports: [TypeOrmModule.forFeature([AuthLog])],
imports: [TypeOrmModule.forFeature([LoginLog])],
controllers: [LogController],
providers: [LogService],
exports: [LogService],

View File

@ -5,11 +5,13 @@ 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';
import { LoginLog } from './entities/loginLog.entity';
import uaParser from 'ua-parser-js';
import axios from 'axios';
@Injectable()
export class LogService extends BaseService {
constructor(@InjectRepository(AuthLog) private logRepository: Repository<AuthLog>) {
constructor(@InjectRepository(LoginLog) private loginLogRepository: Repository<LoginLog>) {
super();
}
@ -17,39 +19,97 @@ export class LogService extends BaseService {
*
*/
async create(createLogDto: CreateLogDto) {
const log = this.logRepository.create();
await this.logRepository.save(log);
const log = this.loginLogRepository.create();
await this.loginLogRepository.save(log);
return log.id;
}
/**
*
*/
async addLoginLog(log: { nickname: string; status: boolean, description: string; ip: string; userAgent: string }) {
const { nickname, status, description, ip, userAgent } = log;
const { browser, os } = this.parseUserAgent(userAgent);
const { addr } = await this.parseUserIp(ip);
const loginLog = this.loginLogRepository.create({
nickname,
status,
description,
ip,
addr,
browser,
os,
});
await this.loginLogRepository.save(loginLog);
return loginLog.id;
}
/**
*
*/
parseUserAgent(userAgent: string) {
const ua = uaParser(userAgent);
const { browser, os } = ua;
return {
browser: `${browser.name} ${browser.version}`,
os: `${os.name} ${os.version}`,
};
}
/**
* IP
*/
async parseUserIp(ip: string) {
const result = {
addr: '',
};
try {
const url = 'https://whois.pconline.com.cn/ipJson.jsp';
const params = { ip, json: true };
const { data } = await axios.get(url, { params, responseType: 'arraybuffer' });
const dataStr = new TextDecoder('gbk').decode(data);
const parased = JSON.parse(dataStr);
result.addr = parased.addr;
} catch {
result.addr = '未知';
}
return result;
}
/**
* /
*/
async findMany(findLogdto: FindLogDto) {
const { page, size } = findLogdto;
const { page, size, nickname } = findLogdto;
const { skip, take } = this.formatPagination(page, size, true);
return this.logRepository.findAndCount({ skip, take });
return this.loginLogRepository.findAndCount({
skip,
take,
where: {
nickname: nickname ? Like(`%${nickname}%`) : undefined,
}
});
}
/**
* ID
*/
findOne(idOrOptions: number | Partial<AuthLog>) {
findOne(idOrOptions: number | Partial<LoginLog>) {
const where = typeof idOrOptions === 'number' ? { id: idOrOptions } : (idOrOptions as any);
return this.logRepository.findOne({ where });
return this.loginLogRepository.findOne({ where });
}
/**
* ID
*/
update(id: number, updateLogDto: UpdateLogDto) {
// return this.logRepository.update();
// return this.loginLogRepository.update();
}
/**
* ID()
*/
remove(id: number) {
return this.logRepository.softDelete(id);
return this.loginLogRepository.softDelete(id);
}
}