feat(server): api endpoint to get Plaid link token

This commit is contained in:
Ahmed Bouhuolia
2024-01-30 22:51:55 +02:00
parent ba3ea93a2d
commit b9886cfac3
10 changed files with 271 additions and 2 deletions

View File

@@ -91,6 +91,7 @@
"pluralize": "^8.0.0",
"pug": "^3.0.2",
"puppeteer": "^10.2.0",
"plaid": "^10.3.0",
"qim": "0.0.52",
"ramda": "^0.27.1",
"rate-limiter-flexible": "^2.1.14",

View File

@@ -0,0 +1,18 @@
import Container, { Inject, Service } from 'typedi';
import { Router } from 'express';
import BaseController from '@/api/controllers/BaseController';
import { PlaidBankingController } from './PlaidBankingController';
@Service()
export class BankingController extends BaseController {
/**
* Router constructor.
*/
router() {
const router = Router();
router.use('/plaid', Container.get(PlaidBankingController).router());
return router;
}
}

View File

@@ -0,0 +1,29 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response } from 'express';
import BaseController from '@/api/controllers/BaseController';
import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
@Service()
export class PlaidBankingController extends BaseController {
@Inject()
private plaidApp: PlaidApplication;
/**
* Router constructor.
*/
router() {
const router = Router();
router.post('/link-token', this.linkToken.bind(this));
return router;
}
private async linkToken(req: Request, res: Response) {
const { tenantId } = req;
const linkToken = await this.plaidApp.getLinkToken(tenantId);
return res.status(200).send(linkToken);
}
}

View File

@@ -56,6 +56,7 @@ import { ProjectsController } from './controllers/Projects/Projects';
import { ProjectTasksController } from './controllers/Projects/Tasks';
import { ProjectTimesController } from './controllers/Projects/Times';
import { TaxRatesController } from './controllers/TaxRates/TaxRates';
import { BankingController } from './controllers/Banking/BankingController';
export default () => {
const app = Router();
@@ -118,6 +119,7 @@ export default () => {
Container.get(InventoryItemsCostController).router()
);
dashboard.use('/cashflow', Container.get(CashflowController).router());
dashboard.use('/banking', Container.get(BankingController).router());
dashboard.use('/roles', Container.get(RolesController).router());
dashboard.use(
'/transactions-locking',

View File

@@ -177,6 +177,18 @@ module.exports = {
service: 'open-exchange-rate',
openExchangeRate: {
appId: process.env.OPEN_EXCHANGE_RATE_APP_ID,
}
}
},
},
/**
* Plaid.
*/
plaid: {
env: process.env.PLAID_ENV || 'sandbox',
clientId: process.env.PLAID_CLIENT_ID,
secretDevelopment: process.env.PLAID_SECRET_DEVELOPMENT,
secretSandbox: process.env.PLAID_SECRET_SANDBOX,
redirectSandBox: process.env.PLAID_SANDBOX_REDIRECT_URI,
redirectDevelopment: process.env.PLAID_DEVELOPMENT_REDIRECT_URI,
},
};

View File

@@ -0,0 +1,103 @@
import { forEach } from 'lodash';
import { Configuration, PlaidApi, PlaidEnvironments } from 'plaid';
import { createPlaidApiEvent } from './PlaidApiEventsDBSync';
import config from '@/config';
const OPTIONS = { clientApp: 'Plaid-Pattern' };
// We want to log requests to / responses from the Plaid API (via the Plaid client), as this data
// can be useful for troubleshooting.
/**
* Logging function for Plaid client methods that use an access_token as an argument. Associates
* the Plaid API event log entry with the item and user the request is for.
*
* @param {string} clientMethod the name of the Plaid client method called.
* @param {Array} clientMethodArgs the arguments passed to the Plaid client method.
* @param {Object} response the response from the Plaid client.
*/
const defaultLogger = async (clientMethod, clientMethodArgs, response) => {
const accessToken = clientMethodArgs[0].access_token;
// const { id: itemId, user_id: userId } = await retrieveItemByPlaidAccessToken(
// accessToken
// );
// await createPlaidApiEvent(1, 1, clientMethod, clientMethodArgs, response);
console.log(response);
};
/**
* Logging function for Plaid client methods that do not use access_token as an argument. These
* Plaid API event log entries will not be associated with an item or user.
*
* @param {string} clientMethod the name of the Plaid client method called.
* @param {Array} clientMethodArgs the arguments passed to the Plaid client method.
* @param {Object} response the response from the Plaid client.
*/
const noAccessTokenLogger = async (
clientMethod,
clientMethodArgs,
response
) => {
console.log(response);
// await createPlaidApiEvent(
// undefined,
// undefined,
// clientMethod,
// clientMethodArgs,
// response
// );
};
// Plaid client methods used in this app, mapped to their appropriate logging functions.
const clientMethodLoggingFns = {
accountsGet: defaultLogger,
institutionsGet: noAccessTokenLogger,
institutionsGetById: noAccessTokenLogger,
itemPublicTokenExchange: noAccessTokenLogger,
itemRemove: defaultLogger,
linkTokenCreate: noAccessTokenLogger,
transactionsSync: defaultLogger,
sandboxItemResetLogin: defaultLogger,
};
// Wrapper for the Plaid client. This allows us to easily log data for all Plaid client requests.
export class PlaidClientWrapper {
constructor() {
// Initialize the Plaid client.
const configuration = new Configuration({
basePath: PlaidEnvironments[config.plaid.env],
baseOptions: {
headers: {
'PLAID-CLIENT-ID': config.plaid.clientId,
'PLAID-SECRET':
config.plaid.env === 'development'
? config.plaid.secretDevelopment
: config.plaid.secretSandbox,
'Plaid-Version': '2020-09-14',
},
},
});
this.client = new PlaidApi(configuration);
// Wrap the Plaid client methods to add a logging function.
forEach(clientMethodLoggingFns, (logFn, method) => {
this[method] = this.createWrappedClientMethod(method, logFn);
});
}
// Allows us to log API request data for troubleshooting purposes.
createWrappedClientMethod(clientMethod, log) {
return async (...args) => {
try {
const res = await this.client[clientMethod](...args);
await log(clientMethod, args, res);
return res;
} catch (err) {
await log(clientMethod, args, err?.response?.data);
throw err;
}
};
}
}

View File

@@ -0,0 +1,48 @@
/**
* Creates a single Plaid api event log entry.
*
* @param {string} itemId the item id in the request.
* @param {string} userId the user id in the request.
* @param {string} plaidMethod the Plaid client method called.
* @param {Array} clientMethodArgs the arguments passed to the Plaid client method.
* @param {Object} response the Plaid api response object.
*/
export const createPlaidApiEvent = async (
itemId,
userId,
plaidMethod,
clientMethodArgs,
response
) => {
const {
error_code: errorCode,
error_type: errorType,
request_id: requestId,
} = response;
const query = {
text: `
INSERT INTO plaid_api_events_table
(
item_id,
user_id,
plaid_method,
arguments,
request_id,
error_type,
error_code
)
VALUES
($1, $2, $3, $4, $5, $6, $7);
`,
values: [
itemId,
userId,
plaidMethod,
JSON.stringify(clientMethodArgs),
requestId,
errorType,
errorCode,
],
};
// await db.query(query);
};

View File

@@ -0,0 +1 @@
export * from './Plaid';

View File

@@ -0,0 +1,18 @@
import { Inject, Service } from 'typedi';
import { PlaidLinkTokenService } from './PlaidLinkToken';
@Service()
export class PlaidApplication {
@Inject()
private getLinkTokenService: PlaidLinkTokenService;
/**
*
* @param tenantId
* @param itemId
* @returns
*/
public getLinkToken(tenantId: number) {
return this.getLinkTokenService.getLinkToken(tenantId);
}
}

View File

@@ -0,0 +1,37 @@
import { PlaidClientWrapper } from '@/lib/Plaid';
import { Service } from 'typedi';
@Service()
export class PlaidLinkTokenService {
/**
* Retrieves the plaid link token.
* @param {number} tenantId
* @returns
*/
async getLinkToken(tenantId: number) {
const accessToken = null;
// must include transactions in order to receive transactions webhooks
const products = ['transactions'];
const linkTokenParams = {
user: {
// This should correspond to a unique id for the current user.
client_user_id: 'uniqueId' + tenantId,
},
client_name: 'Pattern',
products,
country_codes: ['US'],
language: 'en',
// webhook: httpTunnel.public_url + '/services/webhook',
access_token: accessToken,
};
// If user has entered a redirect uri in the .env file
// if (redirect_uri.indexOf('http') === 0) {
// linkTokenParams.redirect_uri = redirect_uri;
// }
const plaidInstance = new PlaidClientWrapper();
const createResponse = await plaidInstance.linkTokenCreate(linkTokenParams);
return createResponse.data;
}
}