From d6ea4d29fe302b7a317e03503225270c81fccbc8 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sun, 30 Jan 2022 21:54:30 +0100 Subject: [PATCH] feat(api/private/me): include authProvider in UserInfo This information is supposed to be used by the frontend to identify the login method that was used. The used login method is saved as a string into the session data and extracted via a new SessionAuthProvider decorator. Signed-off-by: David Mehren --- src/api/private/auth/auth.controller.ts | 9 ++++- src/api/private/me/me.controller.ts | 10 ++++-- .../utils/session-authprovider.decorator.ts | 34 +++++++++++++++++++ src/users/user-info.dto.ts | 9 +++++ src/users/users.service.ts | 10 +++++- test/private-api/me.e2e-spec.ts | 6 ++-- .../register-and-login.e2e-spec.ts | 1 + 7 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 src/api/utils/session-authprovider.decorator.ts diff --git a/src/api/private/auth/auth.controller.ts b/src/api/private/auth/auth.controller.ts index 84352b6cb..7f509a970 100644 --- a/src/api/private/auth/auth.controller.ts +++ b/src/api/private/auth/auth.controller.ts @@ -75,11 +75,18 @@ export class AuthController { @Post('local/login') @OpenApi(201, 400, 401) login( - @Req() request: Request & { session: { user: string } }, + @Req() + request: Request & { + session: { + authProvider: string; + 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; + request.session.authProvider = 'local'; } @UseGuards(SessionGuard) diff --git a/src/api/private/me/me.controller.ts b/src/api/private/me/me.controller.ts index 9a93bff17..f8df2d8d4 100644 --- a/src/api/private/me/me.controller.ts +++ b/src/api/private/me/me.controller.ts @@ -10,11 +10,12 @@ import { SessionGuard } from '../../../identity/session.guard'; import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { MediaUploadDto } from '../../../media/media-upload.dto'; import { MediaService } from '../../../media/media.service'; -import { FullUserInfoDto } from '../../../users/user-info.dto'; +import { UserLoginInfoDto } from '../../../users/user-info.dto'; import { User } from '../../../users/user.entity'; import { UsersService } from '../../../users/users.service'; import { OpenApi } from '../../utils/openapi.decorator'; import { RequestUser } from '../../utils/request-user.decorator'; +import { SessionAuthProvider } from '../../utils/session-authprovider.decorator'; @UseGuards(SessionGuard) @OpenApi(401) @@ -31,8 +32,11 @@ export class MeController { @Get() @OpenApi(200) - getMe(@RequestUser() user: User): FullUserInfoDto { - return this.userService.toFullUserDto(user); + getMe( + @RequestUser() user: User, + @SessionAuthProvider() authProvider: string, + ): UserInfoDto { + return this.userService.toUserLoginInfoDto(user, authProvider); } @Get('media') diff --git a/src/api/utils/session-authprovider.decorator.ts b/src/api/utils/session-authprovider.decorator.ts new file mode 100644 index 000000000..6268b7470 --- /dev/null +++ b/src/api/utils/session-authprovider.decorator.ts @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { + createParamDecorator, + ExecutionContext, + InternalServerErrorException, +} from '@nestjs/common'; +import { Request } from 'express'; + +/** + * Extracts the auth provider identifier from a session inside a request + * + * Will throw an {@link InternalServerErrorException} if no identifier is present + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export const SessionAuthProvider = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + const request: Request & { + session: { + authProvider: string; + }; + } = ctx.switchToHttp().getRequest(); + if (!request.session?.authProvider) { + // We should have an auth provider here, otherwise something is wrong + throw new InternalServerErrorException( + 'Session is missing an auth provider identifier', + ); + } + return request.session.authProvider; + }, +); diff --git a/src/users/user-info.dto.ts b/src/users/user-info.dto.ts index 882a6d86c..29027128d 100644 --- a/src/users/user-info.dto.ts +++ b/src/users/user-info.dto.ts @@ -51,3 +51,12 @@ export class FullUserInfoDto extends UserInfoDto { @IsString() email: string; } + +export class UserLoginInfoDto extends UserInfoDto { + /** + * Identifier of the auth provider that was used to log in + */ + @ApiProperty() + @IsString() + authProvider: string; +} diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 140388b91..ec7d875e2 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -9,7 +9,11 @@ import { Repository } from 'typeorm'; import { AlreadyInDBError, NotInDBError } from '../errors/errors'; import { ConsoleLoggerService } from '../logger/console-logger.service'; -import { FullUserInfoDto, UserInfoDto } from './user-info.dto'; +import { + FullUserInfoDto, + UserInfoDto, + UserLoginInfoDto, +} from './user-info.dto'; import { UserRelationEnum } from './user-relation.enum'; import { User } from './user.entity'; @@ -131,4 +135,8 @@ export class UsersService { email: user.email ?? '', }; } + + toUserLoginInfoDto(user: User, authProvider: string): UserLoginInfoDto { + return { ...this.toUserDto(user), authProvider }; + } } diff --git a/test/private-api/me.e2e-spec.ts b/test/private-api/me.e2e-spec.ts index f737afb56..1ba0b6dc5 100644 --- a/test/private-api/me.e2e-spec.ts +++ b/test/private-api/me.e2e-spec.ts @@ -8,7 +8,7 @@ import request from 'supertest'; import { NotInDBError } from '../../src/errors/errors'; import { Note } from '../../src/notes/note.entity'; -import { FullUserInfoDto } from '../../src/users/user-info.dto'; +import { UserLoginInfoDto } from '../../src/users/user-info.dto'; import { User } from '../../src/users/user.entity'; import { TestSetup, TestSetupBuilder } from '../test-setup'; @@ -50,12 +50,12 @@ describe('Me', () => { }); it('GET /me', async () => { - const userInfo = testSetup.userService.toFullUserDto(user); + const userInfo = testSetup.userService.toUserLoginInfoDto(user, 'local'); const response = await agent .get('/api/private/me') .expect('Content-Type', /json/) .expect(200); - const gotUser = response.body as FullUserInfoDto; + const gotUser = response.body as UserLoginInfoDto; expect(gotUser).toEqual(userInfo); }); diff --git a/test/private-api/register-and-login.e2e-spec.ts b/test/private-api/register-and-login.e2e-spec.ts index 04972d390..79a75484a 100644 --- a/test/private-api/register-and-login.e2e-spec.ts +++ b/test/private-api/register-and-login.e2e-spec.ts @@ -55,6 +55,7 @@ describe('Register and Login', () => { const profile = await session.get('/api/private/me').expect(200); expect(profile.body.username).toEqual(USERNAME); expect(profile.body.displayName).toEqual(DISPLAYNAME); + expect(profile.body.authProvider).toEqual('local'); // logout again await session.delete('/api/private/auth/logout').expect(204);