diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 0622f1116..c797912be 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -200,10 +200,11 @@ export class AuthService { throw new UnauthorizedException( 'Two-factor authentication code is invalid', ); - return this.prisma.users.update({ + const result = await this.prisma.users.update({ where: { id: userId }, data: { twoFactorEnabled: true }, }); + return this.prisma.expose(result); } async loginWithTotp( diff --git a/src/modules/multi-factor-authentication/multi-factor-authentication.controller.ts b/src/modules/multi-factor-authentication/multi-factor-authentication.controller.ts new file mode 100644 index 000000000..1047b257d --- /dev/null +++ b/src/modules/multi-factor-authentication/multi-factor-authentication.controller.ts @@ -0,0 +1,46 @@ +import { + Body, + Controller, + Delete, + Param, + ParseIntPipe, + Post, +} from '@nestjs/common'; +import { users } from '@prisma/client'; +import { Expose } from 'src/modules/prisma/prisma.interface'; +import { Scopes } from '../auth/scope.decorator'; +import { EnableTwoFactorAuthenticationDto } from './multi-factor-authentication.dto'; +import { MultiFactorAuthenticationService } from './multi-factor-authentication.service'; + +@Controller('users/:userId/multi-factor-authentication') +export class MultiFactorAuthenticationController { + constructor( + private multiFactorAuthenticationService: MultiFactorAuthenticationService, + ) {} + + @Post('2fa') + @Scopes('user-{userId}:write-2fa') + async enable2FA( + @Param('userId', ParseIntPipe) userId: number, + @Body() body: EnableTwoFactorAuthenticationDto, + ): Promise | string> { + if (body.token) + return this.multiFactorAuthenticationService.enableTwoFactorAuthentication( + userId, + body.token, + ); + return this.multiFactorAuthenticationService.requestTwoFactorAuthentication( + userId, + ); + } + + @Delete('2fa') + @Scopes('user-{userId}:delete-2fa') + async disable2FA( + @Param('userId', ParseIntPipe) userId: number, + ): Promise> { + return this.multiFactorAuthenticationService.disableTwoFactorAuthentication( + userId, + ); + } +} diff --git a/src/modules/multi-factor-authentication/multi-factor-authentication.dto.ts b/src/modules/multi-factor-authentication/multi-factor-authentication.dto.ts new file mode 100644 index 000000000..bb25e6ac9 --- /dev/null +++ b/src/modules/multi-factor-authentication/multi-factor-authentication.dto.ts @@ -0,0 +1,7 @@ +import { IsOptional, IsString } from 'class-validator'; + +export class EnableTwoFactorAuthenticationDto { + @IsString() + @IsOptional() + token?: string; +} diff --git a/src/modules/multi-factor-authentication/multi-factor-authentication.module.ts b/src/modules/multi-factor-authentication/multi-factor-authentication.module.ts new file mode 100644 index 000000000..1cd91c2fd --- /dev/null +++ b/src/modules/multi-factor-authentication/multi-factor-authentication.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { AuthModule } from '../auth/auth.module'; +import { PrismaModule } from '../prisma/prisma.module'; +import { MultiFactorAuthenticationController } from './multi-factor-authentication.controller'; +import { MultiFactorAuthenticationService } from './multi-factor-authentication.service'; + +@Module({ + imports: [PrismaModule, AuthModule], + controllers: [MultiFactorAuthenticationController], + providers: [MultiFactorAuthenticationService], +}) +export class MultiFactorAuthenticationModule {} diff --git a/src/modules/multi-factor-authentication/multi-factor-authentication.service.ts b/src/modules/multi-factor-authentication/multi-factor-authentication.service.ts new file mode 100644 index 000000000..1ddc8da55 --- /dev/null +++ b/src/modules/multi-factor-authentication/multi-factor-authentication.service.ts @@ -0,0 +1,43 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { users } from '@prisma/client'; +import { AuthService } from '../auth/auth.service'; +import { Expose } from '../prisma/prisma.interface'; +import { PrismaService } from '../prisma/prisma.service'; + +@Injectable() +export class MultiFactorAuthenticationService { + constructor(private prisma: PrismaService, private auth: AuthService) {} + + async requestTwoFactorAuthentication(userId: number): Promise { + const enabled = await this.prisma.users.findOne({ + where: { id: userId }, + select: { twoFactorEnabled: true }, + }); + if (enabled.twoFactorEnabled) + throw new BadRequestException( + 'Two-factor authentication is already enabled', + ); + return this.auth.getTotpQrCode(userId); + } + + async enableTwoFactorAuthentication( + userId: number, + token: string, + ): Promise> { + return this.auth.enableTotp(userId, token); + } + + async disableTwoFactorAuthentication(userId: number): Promise> { + const enabled = await this.prisma.users.findOne({ + where: { id: userId }, + select: { twoFactorEnabled: true }, + }); + if (!enabled.twoFactorEnabled) + throw new BadRequestException('Two-factor authentication is not enabled'); + const user = await this.prisma.users.update({ + where: { id: userId }, + data: { twoFactorEnabled: false, twoFactorSecret: null }, + }); + return this.prisma.expose(user); + } +}