From d0bb693c4c9ec942140cef509bc63763dc3906f2 Mon Sep 17 00:00:00 2001 From: Anand Chowdhary Date: Fri, 30 Oct 2020 13:03:34 +0530 Subject: [PATCH] :recycle: Add cache to geolocation --- src/config/configuration.ts | 3 +++ src/modules/auth/auth.module.ts | 2 ++ src/modules/auth/auth.service.ts | 13 ++++++++++++- src/modules/geolocation/geolocation.module.ts | 2 ++ src/modules/geolocation/geolocation.service.ts | 16 ++++++++++++++++ src/templates/auth/approve-subnet.md | 2 +- 6 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/config/configuration.ts b/src/config/configuration.ts index b35d5d1b3..fc155849b 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -3,6 +3,9 @@ export default () => ({ meta: { totpServiceName: process.env.TOPT_SERVICE_NAME ?? 'Staart', }, + caching: { + geolocationLruSize: process.env.GEOLOCATION_LRU_SIZE ?? 100, + }, security: { saltRounds: process.env.SALT_ROUNDS ?? 10, jwtSecret: process.env.JWT_SECRET ?? 'staart', diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index b208cd78e..6f887f1cc 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -3,6 +3,7 @@ import { ConfigModule } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; import { EmailModule } from '../email/email.module'; +import { GeolocationModule } from '../geolocation/geolocation.module'; import { PrismaModule } from '../prisma/prisma.module'; import { PwnedModule } from '../pwned/pwned.module'; import { TokensModule } from '../tokens/tokens.module'; @@ -18,6 +19,7 @@ import { JwtStrategy } from './jwt.strategy'; TokensModule, ConfigModule, PwnedModule, + GeolocationModule, JwtModule.register({ secret: process.env.JWT_SECRET ?? 'staart', }), diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 2f359df31..7b25e8c45 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -30,6 +30,7 @@ import { TokensService } from '../tokens/tokens.service'; import { RegisterDto } from './auth.dto'; import { AccessTokenClaims } from './auth.interface'; import anonymize from 'ip-anonymize'; +import { GeolocationService } from '../geolocation/geolocation.service'; @Injectable() export class AuthService { @@ -42,6 +43,7 @@ export class AuthService { private jwtService: JwtService, private pwnedService: PwnedService, private tokensService: TokensService, + private geolocationService: GeolocationService, ) { this.authenticator = authenticator.create({ window: [ @@ -366,12 +368,21 @@ export class AuthService { select: { name: true, prefersEmail: true }, }); if (!user) throw new NotFoundException('User not found'); + const location = await this.geolocationService.getLocation(ipAddress); + const locationName = + [ + location?.city?.names?.en, + location?.subdivisions[0]?.names?.en, + location?.country?.names?.en, + ] + .filter(i => i) + .join(', ') || 'Unknown location'; this.email.send({ to: `"${user.name}" <${user.prefersEmail.emailSafe}>`, template: 'auth/approve-subnets', data: { name: user.name, - subnet, + locationName, minutes: 30, link: `${this.configService.get( 'frontendUrl', diff --git a/src/modules/geolocation/geolocation.module.ts b/src/modules/geolocation/geolocation.module.ts index 8198640ef..fb715da2a 100644 --- a/src/modules/geolocation/geolocation.module.ts +++ b/src/modules/geolocation/geolocation.module.ts @@ -1,7 +1,9 @@ import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; import { GeolocationService } from './geolocation.service'; @Module({ + imports: [ConfigModule], providers: [GeolocationService], exports: [GeolocationService], }) diff --git a/src/modules/geolocation/geolocation.service.ts b/src/modules/geolocation/geolocation.service.ts index 7d2f5426c..c79a84a1a 100644 --- a/src/modules/geolocation/geolocation.service.ts +++ b/src/modules/geolocation/geolocation.service.ts @@ -1,10 +1,17 @@ import { Injectable, OnModuleDestroy } from '@nestjs/common'; import maxmind, { Reader, CityResponse } from 'maxmind'; import geolite2 from 'geolite2-redist'; +import QuickLRU from 'quick-lru'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class GeolocationService implements OnModuleDestroy { + constructor(private configService: ConfigService) {} + lookup: Reader | null = null; + lru = new QuickLRU>({ + maxSize: this.configService.get('caching.geolocationLruSize'), + }); onModuleDestroy() { if (this.lookup) this.lookup = null; @@ -12,6 +19,15 @@ export class GeolocationService implements OnModuleDestroy { /** Get the geolocation from an IP address */ async getLocation(ipAddress: string): Promise> { + if (this.lru.has(ipAddress)) return this.lru.get(ipAddress); + const result = await this.getSafeLocation(ipAddress); + this.lru.set(ipAddress, result); + return result; + } + + private async getSafeLocation( + ipAddress: string, + ): Promise> { try { return this.getUnsafeLocation(ipAddress); } catch (error) { diff --git a/src/templates/auth/approve-subnet.md b/src/templates/auth/approve-subnet.md index 61be88dab..90626aa84 100644 --- a/src/templates/auth/approve-subnet.md +++ b/src/templates/auth/approve-subnet.md @@ -2,7 +2,7 @@ Hi {{name}}, -Someone (hopefully you) logged in to your account from a new location ({{ subnet }}), so you'll have to approve it. +Someone (hopefully you) logged in to your account from a new location ({{ locationName }}), so you'll have to approve it. Approve this login