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" "endOfLine": "auto"
}, },
"dependencies": { "dependencies": {
"@nestjs/axios": "^3.0.0",
"@nestjs/common": "^9.0.0", "@nestjs/common": "^9.0.0",
"@nestjs/core": "^9.0.0", "@nestjs/core": "^9.0.0",
"@nestjs/platform-express": "^9.0.0", "@nestjs/platform-express": "^9.0.0",
"axios": "^1.5.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.2.0" "rxjs": "^7.2.0",
"ua-parser-js": "^1.0.36"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cache-manager": "^2.1.0", "@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 { AuthUserDto } from './dto/auth-user.dto';
import { Public } from './jwt'; import { Public } from './jwt';
import { LoginedUserVo } from './vo/logined-user.vo'; import { LoginedUserVo } from './vo/logined-user.vo';
import { AuthLogInterceptor } from '@/monitor/log'; import { LoginLogInterceptor } from '@/monitor/log';
@ApiTags('auth') @ApiTags('auth')
@Controller('auth') @Controller('auth')
@ -17,7 +17,7 @@ export class AuthController {
@Post('login') @Post('login')
@Public() @Public()
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@UseInterceptors(AuthLogInterceptor) @UseInterceptors(LoginLogInterceptor)
login(@Body() user: AuthUserDto): Promise<LoginedUserVo> { login(@Body() user: AuthUserDto): Promise<LoginedUserVo> {
return this.authService.signIn(user); return this.authService.signIn(user);
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { SetMetadata } from '@nestjs/common'; 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 * @param permissions
* @returns * @returns
*/ */
export function NeedPermission(...permissions: PermissionEnum[]) { export function PermissionWith(...permissions: string[]) {
return SetMetadata(PERMISSION_KEY, permissions); return SetMetadata(PERMISSION_KEY, permissions);
} }

View File

@ -1,13 +1,15 @@
import { PaginationDto } from '@/common/response'; import { PaginationDto } from '@/common/response';
import { IntersectionType } from '@nestjs/swagger'; import { IntersectionType } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsOptional, IsString } from 'class-validator'; import { IsOptional, IsString } from 'class-validator';
export class FindLogDto extends IntersectionType(PaginationDto) { export class FindLogDto extends IntersectionType(PaginationDto) {
/** /**
* (Swagger) *
* @example '示例值' * @example '绝弹'
*/ */
@IsOptional() @IsOptional()
@IsString() @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'; import { Column, Entity } from 'typeorm';
@Entity({ orderBy: { id: 'DESC' } }) @Entity({ orderBy: { id: 'DESC' } })
export class AuthLog extends BaseEntity { export class OperationLog extends BaseEntity {
/** /**
* *
* @example '绝弹' * @example '绝弹'

View File

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

View File

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

View File

@ -5,11 +5,13 @@ import { Like, Repository } from 'typeorm';
import { CreateLogDto } from './dto/create-log.dto'; import { CreateLogDto } from './dto/create-log.dto';
import { FindLogDto } from './dto/find-log.dto'; import { FindLogDto } from './dto/find-log.dto';
import { UpdateLogDto } from './dto/update-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() @Injectable()
export class LogService extends BaseService { export class LogService extends BaseService {
constructor(@InjectRepository(AuthLog) private logRepository: Repository<AuthLog>) { constructor(@InjectRepository(LoginLog) private loginLogRepository: Repository<LoginLog>) {
super(); super();
} }
@ -17,39 +19,97 @@ export class LogService extends BaseService {
* *
*/ */
async create(createLogDto: CreateLogDto) { async create(createLogDto: CreateLogDto) {
const log = this.logRepository.create(); const log = this.loginLogRepository.create();
await this.logRepository.save(log); await this.loginLogRepository.save(log);
return log.id; 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) { async findMany(findLogdto: FindLogDto) {
const { page, size } = findLogdto; const { page, size, nickname } = findLogdto;
const { skip, take } = this.formatPagination(page, size, true); 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 * ID
*/ */
findOne(idOrOptions: number | Partial<AuthLog>) { findOne(idOrOptions: number | Partial<LoginLog>) {
const where = typeof idOrOptions === 'number' ? { id: idOrOptions } : (idOrOptions as any); const where = typeof idOrOptions === 'number' ? { id: idOrOptions } : (idOrOptions as any);
return this.logRepository.findOne({ where }); return this.loginLogRepository.findOne({ where });
} }
/** /**
* ID * ID
*/ */
update(id: number, updateLogDto: UpdateLogDto) { update(id: number, updateLogDto: UpdateLogDto) {
// return this.logRepository.update(); // return this.loginLogRepository.update();
} }
/** /**
* ID() * ID()
*/ */
remove(id: number) { remove(id: number) {
return this.logRepository.softDelete(id); return this.loginLogRepository.softDelete(id);
} }
} }