From d10fa350d948f5c698f3c367ca5b45acac10706a Mon Sep 17 00:00:00 2001 From: isra el Date: Sat, 20 Apr 2024 00:45:53 +0300 Subject: [PATCH] feat(api): implement access logging --- api/src/auth/auth.module.ts | 5 +++ api/src/auth/auth.service.ts | 39 +++++++++++++++++------ api/src/auth/guards/auth.guard.ts | 3 +- api/src/auth/schemas/access-log.schema.ts | 31 ++++++++++++++++++ 4 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 api/src/auth/schemas/access-log.schema.ts diff --git a/api/src/auth/auth.module.ts b/api/src/auth/auth.module.ts index 1d170f0..3595527 100644 --- a/api/src/auth/auth.module.ts +++ b/api/src/auth/auth.module.ts @@ -12,6 +12,7 @@ import { PasswordReset, PasswordResetSchema, } from './schemas/password-reset.schema' +import { AccessLog, AccessLogSchema } from './schemas/access-log.schema' @Module({ imports: [ @@ -24,6 +25,10 @@ import { name: PasswordReset.name, schema: PasswordResetSchema, }, + { + name: AccessLog.name, + schema: AccessLogSchema, + }, ]), UsersModule, PassportModule, diff --git a/api/src/auth/auth.service.ts b/api/src/auth/auth.service.ts index 612dfac..ff27127 100644 --- a/api/src/auth/auth.service.ts +++ b/api/src/auth/auth.service.ts @@ -14,6 +14,7 @@ import { } from './schemas/password-reset.schema' import { MailService } from 'src/mail/mail.service' import { RequestResetPasswordInputDTO, ResetPasswordInputDTO } from './auth.dto' +import { AccessLog } from './schemas/access-log.schema' @Injectable() export class AuthService { constructor( @@ -22,6 +23,7 @@ export class AuthService { @InjectModel(ApiKey.name) private apiKeyModel: Model, @InjectModel(PasswordReset.name) private passwordResetModel: Model, + @InjectModel(AccessLog.name) private accessLogModel: Model, private readonly mailService: MailService, ) {} @@ -197,16 +199,35 @@ export class AuthService { await this.apiKeyModel.deleteOne({ _id: apiKeyId }) } - async trackApiKeyUsage(apiKeyId: string) { - this.apiKeyModel - .findByIdAndUpdate( - apiKeyId, - { $inc: { usageCount: 1 }, lastUsedAt: new Date() }, - { new: true }, - ) - .exec() + async trackAccessLog({ request }) { + const { apiKey, user, method, url, ip, headers } = request + const userAgent = headers['user-agent'] + + if (request.apiKey) { + this.apiKeyModel + .findByIdAndUpdate( + apiKey._id, + { $inc: { usageCount: 1 }, lastUsedAt: new Date() }, + { new: true }, + ) + .exec() + .catch((e) => { + console.log('Failed to update api key usage count') + console.log(e) + }) + } + + this.accessLogModel + .create({ + apiKey, + user, + method, + url: url.split('?')[0], + ip, + userAgent, + }) .catch((e) => { - console.log('Failed to track api key usage') + console.log('Failed to track access log') console.log(e) }) } diff --git a/api/src/auth/guards/auth.guard.ts b/api/src/auth/guards/auth.guard.ts index f3ca682..4563f56 100644 --- a/api/src/auth/guards/auth.guard.ts +++ b/api/src/auth/guards/auth.guard.ts @@ -42,7 +42,7 @@ export class AuthGuard implements CanActivate { if (apiKey && bcrypt.compareSync(apiKeyString, apiKey.hashedApiKey)) { userId = apiKey.user - this.authService.trackApiKeyUsage(apiKey._id) + request.apiKey = apiKey } } @@ -50,6 +50,7 @@ export class AuthGuard implements CanActivate { const user = await this.usersService.findOne({ _id: userId }) if (user) { request.user = user + this.authService.trackAccessLog({ request }) return true } } diff --git a/api/src/auth/schemas/access-log.schema.ts b/api/src/auth/schemas/access-log.schema.ts new file mode 100644 index 0000000..8ed639b --- /dev/null +++ b/api/src/auth/schemas/access-log.schema.ts @@ -0,0 +1,31 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' +import { Document, Types } from 'mongoose' +import { User } from '../../users/schemas/user.schema' +import { ApiKey } from './api-key.schema' + +export type AccessLogDocument = AccessLog & Document + +@Schema({ timestamps: true }) +export class AccessLog { + _id?: Types.ObjectId + + @Prop({ type: Types.ObjectId, ref: ApiKey.name }) + apiKey: ApiKey + + @Prop({ type: Types.ObjectId, ref: User.name }) + user: User + + @Prop({ type: String }) + url: string + + @Prop({ type: String }) + method: string + + @Prop({ type: String }) + ip: string + + @Prop({ type: String }) + userAgent: string +} + +export const AccessLogSchema = SchemaFactory.createForClass(AccessLog)