diff --git a/src/modules/approved-subnets/approved-subnets.service.ts b/src/modules/approved-subnets/approved-subnets.service.ts index ece187456..688c98857 100644 --- a/src/modules/approved-subnets/approved-subnets.service.ts +++ b/src/modules/approved-subnets/approved-subnets.service.ts @@ -13,7 +13,7 @@ import { import { Expose } from 'src/modules/prisma/prisma.interface'; import { PrismaService } from '../prisma/prisma.service'; import anonymize from 'ip-anonymize'; -import { hash } from 'bcrypt'; +import { compare, hash } from 'bcrypt'; import { ConfigService } from '@nestjs/config'; import { GeolocationService } from '../geolocation/geolocation.service'; @@ -98,4 +98,23 @@ export class ApprovedSubnetsService { }); return this.prisma.expose(approved); } + + /** + * Upsert a new subnet + * If this subnet already exists, skip; otherwise add it + */ + async upsertNewSubnet( + userId: number, + ipAddress: string, + ): Promise> { + const subnet = anonymize(ipAddress); + const previousSubnets = await this.prisma.approvedSubnets.findMany({ + where: { user: { id: userId } }, + }); + for await (const item of previousSubnets) { + if (await compare(subnet, item.subnet)) + return this.prisma.expose(item); + } + return this.approveNewSubnet(userId, ipAddress); + } } diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index e241109c2..34bd7690d 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -45,8 +45,11 @@ export class AuthController { duration: 60, errorMessage: 'Wait for 60 seconds before trying to create an account', }) - async register(@Body() data: RegisterDto): Promise> { - return this.authService.register(data); + async register( + @Ip() ip: string, + @Body() data: RegisterDto, + ): Promise> { + return this.authService.register(ip, data); } @Post('refresh') diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 6f887f1cc..761f72ca7 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; +import { ApprovedSubnetsModule } from '../approved-subnets/approved-subnets.module'; import { EmailModule } from '../email/email.module'; import { GeolocationModule } from '../geolocation/geolocation.module'; import { PrismaModule } from '../prisma/prisma.module'; @@ -20,6 +21,7 @@ import { JwtStrategy } from './jwt.strategy'; ConfigModule, PwnedModule, GeolocationModule, + ApprovedSubnetsModule, 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 7b25e8c45..9d0f86306 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -31,6 +31,7 @@ import { RegisterDto } from './auth.dto'; import { AccessTokenClaims } from './auth.interface'; import anonymize from 'ip-anonymize'; import { GeolocationService } from '../geolocation/geolocation.service'; +import { ApprovedSubnetsService } from '../approved-subnets/approved-subnets.service'; @Injectable() export class AuthService { @@ -44,6 +45,7 @@ export class AuthService { private pwnedService: PwnedService, private tokensService: TokensService, private geolocationService: GeolocationService, + private approvedSubnetsService: ApprovedSubnetsService, ) { this.authenticator = authenticator.create({ window: [ @@ -53,11 +55,17 @@ export class AuthService { }); } - async validateUser(email: string, password?: string): Promise { + async validateUser(email: string, password?: string) { const emailSafe = safeEmail(email); const user = await this.prisma.users.findFirst({ where: { emails: { some: { emailSafe } } }, - select: { id: true, password: true, emails: true }, + select: { + id: true, + password: true, + emails: true, + twoFactorEnabled: true, + checkLocationOnLogin: true, + }, }); if (!user) throw new HttpException('User not found', HttpStatus.NOT_FOUND); if (!user.emails.find(i => i.emailSafe === emailSafe)?.isVerified) @@ -67,7 +75,12 @@ export class AuthService { 'Logging in without passwords is not supported', HttpStatus.NOT_IMPLEMENTED, ); - if (await compare(password, user.password)) return user.id; + if (await compare(password, user.password)) + return { + id: user.id, + twoFactorEnabled: user.twoFactorEnabled, + checkLocationOnLogin: user.checkLocationOnLogin, + }; return null; } @@ -78,13 +91,22 @@ export class AuthService { password?: string, code?: string, ) { - const id = await this.validateUser(email, password); - if (!id) throw new UnauthorizedException(); - if (code) return this.loginUserWithTotpCode(ipAddress, userAgent, id, code); - return this.loginResponse(ipAddress, userAgent, id); + const user = await this.validateUser(email, password); + if (!user) throw new UnauthorizedException(); + if (code) + return this.loginUserWithTotpCode(ipAddress, userAgent, user.id, code); + if (user.twoFactorEnabled) { + } + await this.checkLoginSubnet( + ipAddress, + userAgent, + user.checkLocationOnLogin, + user.id, + ); + return this.loginResponse(ipAddress, userAgent, user.id); } - async register(data: RegisterDto): Promise> { + async register(ipAddress: string, data: RegisterDto): Promise> { const email = data.email; data.name = data.name .split(' ') @@ -122,6 +144,7 @@ export class AuthService { }, }); await this.sendEmailVerification(email); + await this.approvedSubnetsService.approveNewSubnet(user.id, ipAddress); return this.prisma.expose(user); } @@ -284,6 +307,7 @@ export class AuthService { !!ignorePwnedPassword, ); await this.prisma.users.update({ where: { id }, data: { password } }); + await this.approvedSubnetsService.upsertNewSubnet(id, ipAddress); return this.loginResponse(ipAddress, userAgent, id); }