Skip to content

Commit

Permalink
Make changes needed for frontend login (#66)
Browse files Browse the repository at this point in the history
  • Loading branch information
marode-cap authored Nov 6, 2023
1 parent dea3147 commit c184bc9
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 20 deletions.
6 changes: 3 additions & 3 deletions config/config.dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"SESSION_SECRET": "SessionSecretForDevelopment",
"SESSION_TTL_MS": 3600000,
"BACKEND_ADDRESS": "http://127.0.0.1:9090",
"OIDC_CALLBACK_URL": "http://localhost:9091/api/frontend/login",
"DEFAULT_LOGIN_REDIRECT": "/",
"LOGOUT_REDIRECT": "/"
"OIDC_CALLBACK_URL": "http://localhost:8099/api/frontend/login",
"DEFAULT_LOGIN_REDIRECT": "http://localhost:8099/",
"LOGOUT_REDIRECT": "http://localhost:8099/"
},
"DB": {
"CLIENT_URL": "postgres://admin:[email protected]:5432",
Expand Down
1 change: 1 addition & 0 deletions dev-realm-spsh.json
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,7 @@
"secret": "YDp6fYkbUcj4ZkyAOnbAHGQ9O72htc5M",
"redirectUris": [
"http://localhost:9091/*",
"http://localhost:8099/*",
"/*"
],
"webOrigins": [
Expand Down
23 changes: 19 additions & 4 deletions src/modules/frontend/api/frontend.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,30 @@ import { ConfigTestModule } from '../../../../test/utils/config-test.module.js';
import { FrontendConfig } from '../../../shared/config/frontend.config.js';
import { OIDC_CLIENT } from '../auth/oidc-client.service.js';
import { User } from '../auth/user.decorator.js';
import { ProviderService } from '../outbound/provider.service.js';
import { FrontendController } from './frontend.controller.js';
import { GetServiceProviderInfoDo } from '../../rolle/domain/get-service-provider-info.do.js';

describe('FrontendController', () => {
let module: TestingModule;
let frontendController: FrontendController;
let oidcClient: DeepMocked<Client>;
let frontendConfig: FrontendConfig;
let providerService: DeepMocked<ProviderService>;

beforeAll(async () => {
module = await Test.createTestingModule({
imports: [ConfigTestModule],
providers: [
FrontendController,
{
provide: OIDC_CLIENT,
useValue: createMock<Client>(),
},
{ provide: ProviderService, useValue: createMock<ProviderService>() },
{ provide: OIDC_CLIENT, useValue: createMock<Client>() },
],
}).compile();

frontendController = module.get(FrontendController);
oidcClient = module.get(OIDC_CLIENT);
providerService = module.get(ProviderService);
frontendConfig = module.get(ConfigService).getOrThrow<FrontendConfig>('FRONTEND');
});

Expand Down Expand Up @@ -179,4 +181,17 @@ describe('FrontendController', () => {
expect(result).toBe(user.userinfo);
});
});

describe('provider', () => {
it('should return providers', async () => {
const providers: GetServiceProviderInfoDo[] = [
{ id: faker.string.uuid(), name: faker.hacker.noun(), url: faker.internet.url() },
];
providerService.listProviders.mockResolvedValueOnce(providers);

const result: GetServiceProviderInfoDo[] = await frontendController.provider(createMock<User>());

expect(result).toEqual(providers);
});
});
});
24 changes: 19 additions & 5 deletions src/modules/frontend/api/frontend.controller.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { Controller, Get, Inject, Logger, Post, Req, Res, Session, UseGuards } from '@nestjs/common';
import { Controller, Get, Inject, Logger, Req, Res, Session, UseGuards } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
ApiForbiddenResponse,
ApiInternalServerErrorResponse,
ApiOkResponse,
ApiOperation,
ApiQuery,
ApiResponse,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { Request, Response } from 'express';
import { SessionData } from 'express-session';
import { Client, UserinfoResponse } from 'openid-client';

import { FrontendConfig, ServerConfig } from '../../../shared/config/index.js';
import { GetServiceProviderInfoDo } from '../../rolle/domain/get-service-provider-info.do.js';
import { AuthenticatedGuard, CurrentUser, LoginGuard, OIDC_CLIENT, User } from '../auth/index.js';
import { ProviderService } from '../outbound/provider.service.js';
import { RedirectQueryParams } from './redirect.query.params.js';

@ApiTags('frontend')
Expand All @@ -26,7 +28,11 @@ export class FrontendController {

private readonly logoutRedirect: string;

public constructor(configService: ConfigService<ServerConfig>, @Inject(OIDC_CLIENT) private client: Client) {
public constructor(
configService: ConfigService<ServerConfig>,
@Inject(OIDC_CLIENT) private client: Client,
private providerService: ProviderService,
) {
const frontendConfig: FrontendConfig = configService.getOrThrow<FrontendConfig>('FRONTEND');
this.defaultLoginRedirect = frontendConfig.DEFAULT_LOGIN_REDIRECT;
this.logoutRedirect = frontendConfig.LOGOUT_REDIRECT;
Expand All @@ -43,7 +49,7 @@ export class FrontendController {
res.redirect(target);
}

@Post('logout')
@Get('logout')
@ApiOperation({ summary: 'Used to log out the current user.' })
@ApiResponse({ status: 302, description: 'Redirect to logout.' })
@ApiInternalServerErrorResponse({ description: 'Internal server error while trying to log out.' })
Expand Down Expand Up @@ -81,9 +87,17 @@ export class FrontendController {
@Get('logininfo')
@UseGuards(AuthenticatedGuard)
@ApiOperation({ summary: 'Info about logged in user.' })
@ApiForbiddenResponse({ description: 'User is not logged in.' })
@ApiUnauthorizedResponse({ description: 'User is not logged in.' })
@ApiOkResponse({ description: 'Returns info about the logged in user.' })
public info(@CurrentUser() user: User): UserinfoResponse {
return user.userinfo;
}

@Get('provider')
@UseGuards(AuthenticatedGuard)
@ApiUnauthorizedResponse({ description: 'User is not logged in.' })
@ApiOkResponse({ description: 'Returns the providers for the current user.' })
public provider(@CurrentUser() user: User): Promise<GetServiceProviderInfoDo[]> {
return this.providerService.listProviders(user);
}
}
8 changes: 3 additions & 5 deletions src/modules/frontend/auth/authenticated.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DeepMocked, createMock } from '@golevelup/ts-jest';
import { ExecutionContext } from '@nestjs/common';
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { Request } from 'express';

Expand Down Expand Up @@ -35,13 +35,11 @@ describe('AuthenticatedGuard', () => {
expect(result).toBe(true);
});

it('should return false if user is not authenticated', () => {
it('should throw UnauthorizedException if user is not authenticated', () => {
const contextMock: DeepMocked<ExecutionContext> = createMock();
contextMock.switchToHttp().getRequest<DeepMocked<Request>>().isAuthenticated.mockReturnValueOnce(false);

const result: boolean = sut.canActivate(contextMock);

expect(result).toBe(false);
expect(() => sut.canActivate(contextMock)).toThrow(UnauthorizedException);
});
});
});
10 changes: 8 additions & 2 deletions src/modules/frontend/auth/authenticated.guard.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { Request } from 'express';

@Injectable()
export class AuthenticatedGuard implements CanActivate {
public canActivate(context: ExecutionContext): boolean {
const request: Request = context.switchToHttp().getRequest<Request>();
return request.isAuthenticated();
const isAuthenticated: boolean = request.isAuthenticated();

if (!isAuthenticated) {
throw new UnauthorizedException();
}

return isAuthenticated;
}
}
10 changes: 9 additions & 1 deletion src/modules/frontend/frontend-api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@ import { PassportModule } from '@nestjs/passport';
import { FrontendController } from './api/frontend.controller.js';
import { AuthenticatedGuard, OIDCClientProvider, OpenIdConnectStrategy, SessionSerializer } from './auth/index.js';
import { BackendHttpService } from './outbound/backend-http.service.js';
import { ProviderService } from './outbound/provider.service.js';
@Module({
imports: [HttpModule, PassportModule.register({ session: true, defaultStrategy: 'oidc', keepSessionInfo: true })],
providers: [AuthenticatedGuard, BackendHttpService, OpenIdConnectStrategy, SessionSerializer, OIDCClientProvider],
providers: [
AuthenticatedGuard,
BackendHttpService,
ProviderService,
OpenIdConnectStrategy,
SessionSerializer,
OIDCClientProvider,
],
controllers: [FrontendController],
})
export class FrontendApiModule {}
58 changes: 58 additions & 0 deletions src/modules/frontend/outbound/provider.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { DeepMocked, createMock } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';
import { AxiosResponse } from 'axios';
import { of } from 'rxjs';

import { GetServiceProviderInfoDo } from '../../rolle/domain/get-service-provider-info.do.js';
import { User } from '../auth/user.decorator.js';
import { BackendHttpService } from './backend-http.service.js';
import { ProviderService } from './provider.service.js';

describe('ProviderService', () => {
let module: TestingModule;
let sut: ProviderService;
let httpServiceMock: DeepMocked<BackendHttpService>;

beforeAll(async () => {
module = await Test.createTestingModule({
providers: [
ProviderService,
{
provide: BackendHttpService,
useValue: createMock<BackendHttpService>(),
},
],
}).compile();

sut = module.get(ProviderService);
httpServiceMock = module.get(BackendHttpService);
});

afterAll(async () => {
await module.close();
});

it('should be defined', () => {
expect(sut).toBeDefined();
});

describe('listProviders', () => {
it('should call HttpService.get with params', async () => {
httpServiceMock.get.mockReturnValueOnce(of({ data: [] } as AxiosResponse));
const userMock: User = createMock<User>();

await sut.listProviders(userMock);

expect(httpServiceMock.get).toHaveBeenCalledWith('/api/provider', userMock);
});

it('should return response from service', async () => {
const axiosResponse: AxiosResponse = { data: [] } as AxiosResponse;
httpServiceMock.get.mockReturnValueOnce(of(axiosResponse));

const result: GetServiceProviderInfoDo[] = await sut.listProviders(createMock());

expect(result).toBe(axiosResponse.data);
});
});
});
20 changes: 20 additions & 0 deletions src/modules/frontend/outbound/provider.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Injectable } from '@nestjs/common';
import { AxiosResponse } from 'axios';
import { firstValueFrom, map } from 'rxjs';

import { GetServiceProviderInfoDo } from '../../rolle/domain/get-service-provider-info.do.js';
import { User } from '../auth/user.decorator.js';
import { BackendHttpService } from './backend-http.service.js';

@Injectable()
export class ProviderService {
public constructor(private httpService: BackendHttpService) {}

public async listProviders(user: User): Promise<GetServiceProviderInfoDo[]> {
return firstValueFrom(
this.httpService
.get<GetServiceProviderInfoDo[]>('/api/provider', user)
.pipe(map((res: AxiosResponse<GetServiceProviderInfoDo[]>) => res.data)),
);
}
}

0 comments on commit c184bc9

Please sign in to comment.