Skip to content

Commit

Permalink
Merge pull request #431 from meriamBenSassi/feat/service-account-v2
Browse files Browse the repository at this point in the history
[FEAT] use v2 to fetch service-accounts
  • Loading branch information
meriamBenSassi authored Sep 3, 2024
2 parents d8a1f5d + 0622fcb commit 7c8fdcd
Show file tree
Hide file tree
Showing 19 changed files with 323 additions and 56 deletions.
7 changes: 5 additions & 2 deletions config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"baseUrl": "http://localhost:4000",
"clientId": "a",
"clientSecret": "b",
"debug": false
"debug": false,
"version": 2
},
"bridge":{
"bankinVersion":"2021-06-01",
Expand All @@ -16,7 +17,9 @@
"targetUrl": "http://localhost:8080/hooks",
"eventList": [
"bank_details_required",
"aggregator_link_required"
"aggregator_link_required",
"service_account_updated",
"service_account_created"
],
"restHooksSecret": "a",
"port": 8080,
Expand Down
2 changes: 1 addition & 1 deletion config/env/test.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
],
"bridge":{
"synchronizationWaitingTime": 0,
"synchronizationTimeout": 2
"synchronizationTimeout": 1000
},
"customerIdPassword": "random_pass"
}
2 changes: 1 addition & 1 deletion json-server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const middlewares = jsonServer.defaults();
server.use(middlewares);

// Add custom routes before JSON Server router
server.post('/v1/oauth/token', (req, res) => {
server.post('/v2/oauth/token', (req, res) => {
res.json({
access_token: 'access_token_jwt',
expires_in: 60,
Expand Down
11 changes: 9 additions & 2 deletions src/algoan/algoan.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ import { AlgoanAnalysisService } from './services/algoan-analysis.service';
import { AlgoanCustomerService } from './services/algoan-customer.service';
import { AlgoanHttpService } from './services/algoan-http.service';
import { AlgoanService } from './services/algoan.service';
import { AlgoanServiceAcountService } from './services/algoan-service-account.service';

/**
* Algoan module
*/
@Module({
imports: [ConfigModule],
providers: [AlgoanAnalysisService, AlgoanCustomerService, AlgoanHttpService, AlgoanService],
exports: [AlgoanAnalysisService, AlgoanCustomerService, AlgoanHttpService, AlgoanService],
providers: [
AlgoanAnalysisService,
AlgoanCustomerService,
AlgoanHttpService,
AlgoanService,
AlgoanServiceAcountService,
],
exports: [AlgoanAnalysisService, AlgoanCustomerService, AlgoanHttpService, AlgoanService, AlgoanServiceAcountService],
})
export class AlgoanModule {}
1 change: 1 addition & 0 deletions src/algoan/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './paginated-data';
7 changes: 7 additions & 0 deletions src/algoan/interfaces/paginated-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Paginated Algoan data
*/
export interface PaginatedData<T> {
resources: T[];
totalResources: number;
}
2 changes: 1 addition & 1 deletion src/algoan/services/algoan-http.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class AlgoanHttpService {
clientSecret,
},
{
version: 1,
version: this.config.algoan.version,
},
);
}
Expand Down
63 changes: 63 additions & 0 deletions src/algoan/services/algoan-service-account.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { IServiceAccount, RequestBuilder, ServiceAccount } from '@algoan/rest';
import { Inject, Injectable } from '@nestjs/common';
import { Config } from 'node-config-ts';

import { CONFIG } from '../../config/config.module';
import { PaginatedData } from '../interfaces';
/**
* Service to manage analysis
*/
@Injectable()
export class AlgoanServiceAcountService {
private readonly apiVersion: string = 'v2';
// Note: here we have to instantiate a request builder instead of using the algoanhttp module
// because algoanhttp's scope is REQUEST and we can't use a REQUEST scopped module in a onModuleInit
// and we have to get the service account on initing the module
private readonly requestBuilder = new RequestBuilder(
this.config.algoan.baseUrl,
{
clientId: this.config.algoan.clientId,
clientSecret: this.config.algoan.clientSecret,
},
{
version: this.config.algoan.version,
},
);

constructor(@Inject(CONFIG) private readonly config: Config) {}

/**
* Find all service accounts linekd to the connector
*/
public async findAll(): Promise<ServiceAccount[]> {
const path: string = `/${this.apiVersion}/service-accounts?limit=1000`;
const paginatedServiceAccounts: PaginatedData<IServiceAccount> = await this.requestBuilder.request({
url: path,
method: 'GET',
});

return paginatedServiceAccounts.resources.map(
(sa: IServiceAccount) =>
new ServiceAccount(this.config.algoan.baseUrl, sa, {
apiVersion: this.config.algoan.version,
}),
);
}

/**
* Find a service account by id
*/
public async findById(id: string): Promise<ServiceAccount | undefined> {
/* eslint-disable-next-line @typescript-eslint/naming-convention,camelcase */
const path: string = `/${this.apiVersion}/service-accounts?filter=${JSON.stringify({ _id: id })}`;
const paginatedServiceAccounts: PaginatedData<IServiceAccount> = await this.requestBuilder.request({
url: path,
method: 'GET',
});
if (paginatedServiceAccounts.resources.length > 0) {
return new ServiceAccount(this.config.algoan.baseUrl, paginatedServiceAccounts.resources?.[0], {
apiVersion: this.config.algoan.version,
});
}
}
}
6 changes: 4 additions & 2 deletions src/algoan/services/algoan.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Algoan } from '@algoan/rest';
import { config } from 'node-config-ts';

import { CONFIG } from '../../config/config.module';

import { AlgoanService } from './algoan.service';
import { AlgoanServiceAcountService } from './algoan-service-account.service';

describe('AlgoanService', () => {
let algoanService: AlgoanService;
Expand All @@ -17,6 +17,7 @@ describe('AlgoanService', () => {
provide: CONFIG,
useValue: config,
},
AlgoanServiceAcountService,
],
}).compile();

Expand All @@ -28,7 +29,7 @@ describe('AlgoanService', () => {
});

it('should start properly', async () => {
jest.spyOn(Algoan.prototype, 'initRestHooks').mockReturnValue(Promise.resolve());
jest.spyOn(AlgoanService.prototype, 'initRestHooks').mockReturnValue(Promise.resolve());
await expect(algoanService.onModuleInit()).resolves.toEqual(undefined);
});

Expand All @@ -43,6 +44,7 @@ describe('AlgoanService', () => {
eventList: [],
},
},
AlgoanServiceAcountService,
],
}).compile();

Expand Down
92 changes: 84 additions & 8 deletions src/algoan/services/algoan.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Algoan, EventName } from '@algoan/rest';
import { Algoan, EventName, PostSubscriptionDTO } from '@algoan/rest';
import { Injectable, OnModuleInit, InternalServerErrorException, Inject } from '@nestjs/common';
import { utilities } from 'nest-winston';
import { Config } from 'node-config-ts';
import { format, transports } from 'winston';

import { ServiceAccountUpdatedDTO } from '../../hooks/dto/service-account-updated.dto';
import { ServiceAccountCreatedDTO } from '../../hooks/dto/service-account-created.dto';
import { CONFIG } from '../../config/config.module';
import { AlgoanServiceAcountService } from './algoan-service-account.service';

/**
* Algoan service
Expand All @@ -17,7 +19,10 @@ export class AlgoanService implements OnModuleInit {
*/
public algoanClient!: Algoan;

constructor(@Inject(CONFIG) private readonly config: Config) {}
constructor(
@Inject(CONFIG) private readonly config: Config,
private readonly serviceAccountService: AlgoanServiceAcountService,
) {}

/**
* Fetch services and creates subscription
Expand All @@ -33,7 +38,7 @@ export class AlgoanService implements OnModuleInit {
baseUrl: this.config.algoan.baseUrl,
clientId: this.config.algoan.clientId,
clientSecret: this.config.algoan.clientSecret,
version: 1,
version: this.config.algoan.version,
loggerOptions: {
format:
nodeEnv === 'production' ? format.json() : format.combine(format.timestamp(), utilities.format.nestLike()),
Expand All @@ -53,10 +58,81 @@ export class AlgoanService implements OnModuleInit {
throw new InternalServerErrorException('No event list given');
}

await this.algoanClient.initRestHooks(
this.config.targetUrl,
this.config.eventList as EventName[],
this.config.restHooksSecret,
await this.initRestHooks(this.config.targetUrl, this.config.eventList as EventName[], this.config.restHooksSecret);
}

/**
* Init resthooks in V2
*/
public async initRestHooks(target: string, events: EventName[] = [], secret?: string): Promise<void> {
this.algoanClient.serviceAccounts = await this.serviceAccountService.findAll();

if (this.algoanClient.serviceAccounts.length === 0) {
return;
}
const subscriptionDTO: PostSubscriptionDTO[] = this.fromEventToSubscriptionDTO(target, events, secret);

for (const serviceAccount of this.algoanClient.serviceAccounts) {
await serviceAccount.getOrCreateSubscriptions(subscriptionDTO, events);
}
}

/**
* Transform a list of events to a Subscription request body
* @param target Base URL
* @param eventName List of events
* @param secret Secret
*/
// eslint-disable-next-line
private readonly fromEventToSubscriptionDTO = (
target: string,
events: EventName[],
secret?: string,
): PostSubscriptionDTO[] =>
events.map(
(event: EventName): PostSubscriptionDTO => ({
target,
secret,
eventName: event,
}),
);

/**
* Store the new service account in-memory and create subscriptions
* @param serviceAccount
* @param subscriptionDto
*/
public async saveServiceAccount(payload: ServiceAccountCreatedDTO): Promise<void> {
const addedServiceAccount = await this.serviceAccountService.findById(payload.serviceAccountId);

const eventNames: EventName[] = this.config.eventList as EventName[];

if (addedServiceAccount !== undefined) {
const subscriptionDTO: PostSubscriptionDTO[] = this.fromEventToSubscriptionDTO(
this.config.targetUrl,
eventNames,
this.config.restHooksSecret,
);
this.algoanClient.serviceAccounts.push(addedServiceAccount);
await addedServiceAccount.getOrCreateSubscriptions(subscriptionDTO, eventNames);
}
}

/**
* Update the service account config
* @param payload
*/
public async updateServiceAccount(payload: ServiceAccountUpdatedDTO): Promise<void> {
const updatedServiceAccount = await this.serviceAccountService.findById(payload.serviceAccountId);

if (updatedServiceAccount) {
const oldServiceAccountIdx = this.algoanClient.serviceAccounts.findIndex(
(serviceAccount) => serviceAccount.clientId === updatedServiceAccount?.clientId,
);
// eslint-disable-next-line
if (oldServiceAccountIdx > -1) {
this.algoanClient.serviceAccounts[oldServiceAccountIdx].config = updatedServiceAccount.config;
}
}
}
}
3 changes: 2 additions & 1 deletion src/app.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';

import { AlgoanService } from './algoan/services/algoan.service';
import { AlgoanServiceAcountService } from './algoan/services/algoan-service-account.service';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';
Expand All @@ -12,7 +13,7 @@ describe('AppController', () => {
const app: TestingModule = await Test.createTestingModule({
imports: [ConfigModule],
controllers: [AppController],
providers: [AppService, AlgoanService],
providers: [AppService, AlgoanService, AlgoanServiceAcountService],
}).compile();

appController = app.get<AppController>(AppController);
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/controllers/hooks.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('Hooks Controller', () => {
time: 1586177798388,
index: 32,
id: 'eventId',
};
} as unknown as EventDTO;

const spy = jest.spyOn(hooksService, 'handleWebhook').mockReturnValue(Promise.resolve());
await controller.controlHook(event, {
Expand Down
8 changes: 7 additions & 1 deletion src/hooks/dto/event.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ import { AggregatorLinkRequiredDTO } from './aggregator-link-required.dto';
import { BanksDetailsRequiredDTO } from './bank-details-required.dto';
import { ServiceAccountCreatedDTO } from './service-account-created.dto';
import { ServiceAccountDeletedDTO } from './service-account-deleted.dto';
import { ServiceAccountUpdatedDTO } from './service-account-updated.dto';
import { SubscriptionDTO } from './subscription.dto';

/**
* Events payload types
*/
type Events = ServiceAccountCreatedDTO | ServiceAccountDeletedDTO | AggregatorLinkRequiredDTO | BanksDetailsRequiredDTO;
type Events =
| ServiceAccountCreatedDTO
| ServiceAccountDeletedDTO
| AggregatorLinkRequiredDTO
| BanksDetailsRequiredDTO
| ServiceAccountUpdatedDTO;

/**
* Event
Expand Down
13 changes: 13 additions & 0 deletions src/hooks/dto/service-account-updated.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { IsNotEmpty, IsString } from 'class-validator';

/**
* ServiceAccountUpdated DTO
*/
export class ServiceAccountUpdatedDTO {
/**
* Unique service account DTO
*/
@IsNotEmpty()
@IsString()
public readonly serviceAccountId: string;
}
3 changes: 2 additions & 1 deletion src/hooks/dto/subscription.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IsNotEmpty } from 'class-validator';
import { EventName, SubscriptionStatus } from '@algoan/rest';
import { SubscriptionStatus } from '@algoan/rest';
import { EventName } from '../enums/event-name.enum';

/**
* Subscription
Expand Down
11 changes: 11 additions & 0 deletions src/hooks/enums/event-name.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* eslint-disable @typescript-eslint/naming-convention,camelcase */
/**
* Received events names
*/
export enum EventName {
SERVICE_ACCOUNT_CREATED = 'service_account_created',
SERVICE_ACCOUNT_UPDATED = 'service_account_updated',
SERVICE_ACCOUNT_DELETED = 'service_account_deleted',
AGGREGATOR_LINK_REQUIRED = 'aggregator_link_required',
BANK_DETAILS_REQUIRED = 'bank_details_required',
}
Loading

0 comments on commit 7c8fdcd

Please sign in to comment.