feat: 添加登陆日志功能
parent
a693960017
commit
868769880e
Binary file not shown.
|
|
@ -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",
|
||||
|
|
|
|||
2287
pnpm-lock.yaml
2287
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
|
||||
export class Log {}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 '绝弹'
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
}
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue