mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 14:50:32 +00:00
feat(server): api endpoint to get Plaid link token
This commit is contained in:
@@ -91,6 +91,7 @@
|
|||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"pug": "^3.0.2",
|
"pug": "^3.0.2",
|
||||||
"puppeteer": "^10.2.0",
|
"puppeteer": "^10.2.0",
|
||||||
|
"plaid": "^10.3.0",
|
||||||
"qim": "0.0.52",
|
"qim": "0.0.52",
|
||||||
"ramda": "^0.27.1",
|
"ramda": "^0.27.1",
|
||||||
"rate-limiter-flexible": "^2.1.14",
|
"rate-limiter-flexible": "^2.1.14",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,7 @@ import { ProjectsController } from './controllers/Projects/Projects';
|
|||||||
import { ProjectTasksController } from './controllers/Projects/Tasks';
|
import { ProjectTasksController } from './controllers/Projects/Tasks';
|
||||||
import { ProjectTimesController } from './controllers/Projects/Times';
|
import { ProjectTimesController } from './controllers/Projects/Times';
|
||||||
import { TaxRatesController } from './controllers/TaxRates/TaxRates';
|
import { TaxRatesController } from './controllers/TaxRates/TaxRates';
|
||||||
|
import { BankingController } from './controllers/Banking/BankingController';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const app = Router();
|
const app = Router();
|
||||||
@@ -118,6 +119,7 @@ export default () => {
|
|||||||
Container.get(InventoryItemsCostController).router()
|
Container.get(InventoryItemsCostController).router()
|
||||||
);
|
);
|
||||||
dashboard.use('/cashflow', Container.get(CashflowController).router());
|
dashboard.use('/cashflow', Container.get(CashflowController).router());
|
||||||
|
dashboard.use('/banking', Container.get(BankingController).router());
|
||||||
dashboard.use('/roles', Container.get(RolesController).router());
|
dashboard.use('/roles', Container.get(RolesController).router());
|
||||||
dashboard.use(
|
dashboard.use(
|
||||||
'/transactions-locking',
|
'/transactions-locking',
|
||||||
|
|||||||
@@ -177,6 +177,18 @@ module.exports = {
|
|||||||
service: 'open-exchange-rate',
|
service: 'open-exchange-rate',
|
||||||
openExchangeRate: {
|
openExchangeRate: {
|
||||||
appId: process.env.OPEN_EXCHANGE_RATE_APP_ID,
|
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,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
103
packages/server/src/lib/Plaid/Plaid.ts
Normal file
103
packages/server/src/lib/Plaid/Plaid.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
48
packages/server/src/lib/Plaid/PlaidApiEventsDBSync.ts
Normal file
48
packages/server/src/lib/Plaid/PlaidApiEventsDBSync.ts
Normal 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);
|
||||||
|
};
|
||||||
1
packages/server/src/lib/Plaid/index.ts
Normal file
1
packages/server/src/lib/Plaid/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './Plaid';
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
packages/server/src/services/Banking/Plaid/PlaidLinkToken.ts
Normal file
37
packages/server/src/services/Banking/Plaid/PlaidLinkToken.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user