From b15361563787b4b218b482879fa37e25b4de9741 Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Sun, 8 Aug 2021 22:00:14 +0200 Subject: [PATCH] feat: add auth controller with internal login, registration, password change and logout Signed-off-by: Philip Molares --- src/api/private/auth/auth.controller.spec.ts | 71 +++++++++++++ src/api/private/auth/auth.controller.ts | 105 +++++++++++++++++++ src/api/private/private-api.module.ts | 4 + 3 files changed, 180 insertions(+) create mode 100644 src/api/private/auth/auth.controller.spec.ts create mode 100644 src/api/private/auth/auth.controller.ts diff --git a/src/api/private/auth/auth.controller.spec.ts b/src/api/private/auth/auth.controller.spec.ts new file mode 100644 index 000000000..42fbcbb57 --- /dev/null +++ b/src/api/private/auth/auth.controller.spec.ts @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { ConfigModule } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getConnectionToken, getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import appConfigMock from '../../../config/mock/app.config.mock'; +import authConfigMock from '../../../config/mock/auth.config.mock'; +import { Identity } from '../../../identity/identity.entity'; +import { IdentityModule } from '../../../identity/identity.module'; +import { LoggerModule } from '../../../logger/logger.module'; +import { Session } from '../../../users/session.entity'; +import { User } from '../../../users/user.entity'; +import { UsersModule } from '../../../users/users.module'; +import { AuthController } from './auth.controller'; + +describe('AuthController', () => { + let controller: AuthController; + + type MockConnection = { + transaction: () => void; + }; + + function mockConnection(): MockConnection { + return { + transaction: jest.fn(), + }; + } + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: getRepositoryToken(Identity), + useClass: Repository, + }, + { + provide: getConnectionToken(), + useFactory: mockConnection, + }, + ], + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [appConfigMock, authConfigMock], + }), + LoggerModule, + UsersModule, + IdentityModule, + ], + controllers: [AuthController], + }) + .overrideProvider(getRepositoryToken(Identity)) + .useClass(Repository) + .overrideProvider(getRepositoryToken(Session)) + .useValue({}) + .overrideProvider(getRepositoryToken(User)) + .useValue({}) + .compile(); + + controller = module.get(AuthController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/api/private/auth/auth.controller.ts b/src/api/private/auth/auth.controller.ts new file mode 100644 index 000000000..334a1d29b --- /dev/null +++ b/src/api/private/auth/auth.controller.ts @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { + BadRequestException, + Body, + Controller, + Delete, + NotFoundException, + Post, + Put, + Req, + UseGuards, +} from '@nestjs/common'; +import { Session } from 'express-session'; + +import { AlreadyInDBError, NotInDBError } from '../../../errors/errors'; +import { IdentityService } from '../../../identity/identity.service'; +import { LocalAuthGuard } from '../../../identity/local/local.strategy'; +import { LoginDto } from '../../../identity/local/login.dto'; +import { RegisterDto } from '../../../identity/local/register.dto'; +import { UpdatePasswordDto } from '../../../identity/local/update-password.dto'; +import { SessionGuard } from '../../../identity/session.guard'; +import { ConsoleLoggerService } from '../../../logger/console-logger.service'; +import { User } from '../../../users/user.entity'; +import { UsersService } from '../../../users/users.service'; +import { LoginEnabledGuard } from '../../utils/login-enabled.guard'; +import { RegistrationEnabledGuard } from '../../utils/registration-enabled.guard'; +import { RequestUser } from '../../utils/request-user.decorator'; + +@Controller('auth') +export class AuthController { + constructor( + private readonly logger: ConsoleLoggerService, + private usersService: UsersService, + private identityService: IdentityService, + ) { + this.logger.setContext(AuthController.name); + } + + @UseGuards(RegistrationEnabledGuard) + @Post('local') + async registerUser(@Body() registerDto: RegisterDto): Promise { + try { + const user = await this.usersService.createUser( + registerDto.username, + registerDto.displayname, + ); + // ToDo: Figure out how to rollback user if anything with this calls goes wrong + await this.identityService.createLocalIdentity( + user, + registerDto.password, + ); + return; + } catch (e) { + if (e instanceof AlreadyInDBError) { + throw new BadRequestException(e.message); + } + throw e; + } + } + + @UseGuards(LoginEnabledGuard, SessionGuard) + @Put('local') + async updatePassword( + @RequestUser() user: User, + @Body() changePasswordDto: UpdatePasswordDto, + ): Promise { + try { + await this.identityService.updateLocalPassword( + user, + changePasswordDto.newPassword, + ); + return; + } catch (e) { + if (e instanceof NotInDBError) { + throw new NotFoundException(e.message); + } + throw e; + } + } + + @UseGuards(LoginEnabledGuard, LocalAuthGuard) + @Post('local/login') + login( + @Req() request: Request & { session: { user: string } }, + @Body() loginDto: LoginDto, + ): void { + // There is no further testing needed as we only get to this point if LocalAuthGuard was successful + request.session.user = loginDto.username; + } + + @UseGuards(SessionGuard) + @Delete('logout') + logout(@Req() request: Request & { session: Session }): void { + request.session.destroy((err) => { + if (err) { + this.logger.error('Encountered an error while logging out: ${err}'); + throw new BadRequestException('Unable to log out'); + } + }); + } +} diff --git a/src/api/private/private-api.module.ts b/src/api/private/private-api.module.ts index 98c113e66..625cfbe13 100644 --- a/src/api/private/private-api.module.ts +++ b/src/api/private/private-api.module.ts @@ -8,12 +8,14 @@ import { Module } from '@nestjs/common'; import { AuthModule } from '../../auth/auth.module'; import { FrontendConfigModule } from '../../frontend-config/frontend-config.module'; import { HistoryModule } from '../../history/history.module'; +import { IdentityModule } from '../../identity/identity.module'; import { LoggerModule } from '../../logger/logger.module'; import { MediaModule } from '../../media/media.module'; import { NotesModule } from '../../notes/notes.module'; import { PermissionsModule } from '../../permissions/permissions.module'; import { RevisionsModule } from '../../revisions/revisions.module'; import { UsersModule } from '../../users/users.module'; +import { AuthController } from './auth/auth.controller'; import { ConfigController } from './config/config.controller'; import { HistoryController } from './me/history/history.controller'; import { MeController } from './me/me.controller'; @@ -32,6 +34,7 @@ import { TokensController } from './tokens/tokens.controller'; NotesModule, MediaModule, RevisionsModule, + IdentityModule, ], controllers: [ TokensController, @@ -40,6 +43,7 @@ import { TokensController } from './tokens/tokens.controller'; HistoryController, MeController, NotesController, + AuthController, ], }) export class PrivateApiModule {}