actual-server/app-plaid.js
2022-08-29 23:11:51 -04:00

398 lines
8.8 KiB
JavaScript

// This app is unused right now. Maybe you could use it as a starting
// point for Plaid integration!
const express = require('express');
const uuid = require('uuid');
const fetch = require('node-fetch');
const plaid = require('plaid');
const { middleware: connectDb } = require('./db');
const { handleError } = require('./util/handle-error');
const { validateSubscribedUser } = require('./util/validate-user');
const config = require('./load-config');
const app = express();
let plaidClient;
function init() {
plaidClient = new plaid.Client({
clientID: config.plaid.client_id,
secret: config.plaid.secret,
env: config.plaid.env,
options: { version: '2019-05-29' }
});
}
async function validateToken(req, res) {
let { token } = req.body;
let rows = await req.runQuery(
'SELECT * FROM webTokens WHERE token_id = $1',
[token],
true
);
if (rows.length === 0) {
res.send(JSON.stringify({ status: 'error', reason: 'not-found' }));
return null;
}
// Tokens are only valid for 10 minutes
let validTime = 1000 * 60 * 10;
let row = rows[0];
let timeCreated = JSON.parse(row.time_created);
if (Date.now() - timeCreated >= validTime) {
res.send(JSON.stringify({ status: 'error', reason: 'expired' }));
return null;
}
return row;
}
app.post(
'/create-web-token',
connectDb,
handleError(async (req, res) => {
let user = await validateSubscribedUser(req, res);
if (!user) {
return;
}
let token = uuid.v4();
await req.runQuery('DELETE FROM webTokens WHERE user_id = $1', [user.id]);
await req.runQuery(
'INSERT INTO webTokens (user_id, token_id, time_created) VALUES ($1, $2, $3)',
[user.id, token, Date.now()]
);
res.send(
JSON.stringify({
status: 'ok',
data: token
})
);
})
);
app.post(
'/validate-web-token',
connectDb,
handleError(async (req, res) => {
let token = await validateToken(req, res);
if (!token) {
return;
}
res.send(JSON.stringify({ status: 'ok' }));
})
);
app.post(
'/put-web-token-contents',
connectDb,
handleError(async (req, res) => {
let token = await validateToken(req, res);
if (!token) {
return;
}
let { data } = req.body;
await req.runQuery(
'UPDATE webTokens SET contents = $1 WHERE token_id = $2',
[JSON.stringify(data), token.token_id]
);
res.send(
JSON.stringify({
status: 'ok',
data: null
})
);
})
);
app.post(
'/get-web-token-contents',
connectDb,
handleError(async (req, res) => {
let user = await validateSubscribedUser(req, res);
if (!user) {
return;
}
let token = await validateToken(req, res);
if (!token) {
return;
}
let rows = await req.runQuery(
'SELECT * FROM webTokens WHERE user_id = $1 AND token_id = $2',
[user.id, token.token_id],
true
);
if (rows.length === 0) {
res.send(
JSON.stringify({
status: 'error',
reason: 'not-found'
})
);
}
res.send(
JSON.stringify({
status: 'ok',
data: JSON.parse(rows[0].contents)
})
);
})
);
app.post(
'/make_link_token',
connectDb,
handleError(async (req, res) => {
let token = await validateToken(req, res);
if (!token) {
return;
}
let result = await plaidClient.createLinkToken({
user: {
client_user_id: token.user_id
},
client_name: 'Actual',
products: ['transactions'],
country_codes: ['US'],
language: 'en'
});
res.send(JSON.stringify({ status: 'ok', data: result.link_token }));
})
);
app.post(
'/handoff_public_token',
connectDb,
handleError(async (req, res) => {
let user = await validateSubscribedUser(req, res);
if (!user) {
return;
}
let { item_id, public_token } = req.body;
let url = config.plaid.env + '/item/public_token/exchange';
let resData = await fetch(url, {
method: 'POST',
body: JSON.stringify({
client_id: config.plaid.client_id,
secret: config.plaid.secret,
public_token: public_token
}),
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Actual Budget'
}
}).then((res) => res.json());
await req.runQuery(
'INSERT INTO access_tokens (item_id, user_id, access_token) VALUES ($1, $2, $3)',
[item_id, user.id, resData.access_token]
);
res.send(JSON.stringify({ status: 'ok' }));
})
);
app.post(
'/remove-access-token',
connectDb,
handleError(async (req, res) => {
let user = await validateSubscribedUser(req, res);
if (!user) {
return;
}
let { item_id } = req.body;
const rows = await req.runQuery(
'SELECT * FROM access_tokens WHERE user_id = $1 AND item_id = $2',
[user.id, item_id],
true
);
if (rows.length === 0) {
throw new Error('access token not found');
}
const { access_token } = rows[0];
const url = config.plaid.env + '/item/remove';
const resData = await fetch(url, {
method: 'POST',
body: JSON.stringify({
client_id: config.plaid.client_id,
secret: config.plaid.secret,
access_token: access_token
}),
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Actual Budget'
}
}).then((res) => res.json());
if (resData.removed !== true) {
console.log('[Error] Item not removed: ' + access_token.slice(0, 3));
}
await req.runQuery(
'UPDATE access_tokens SET deleted = TRUE WHERE access_token = $1',
[access_token]
);
res.send(
JSON.stringify({
status: 'ok',
data: resData
})
);
})
);
app.post(
'/accounts',
connectDb,
handleError(async (req, res) => {
let user = await validateSubscribedUser(req, res);
if (!user) {
return;
}
const { item_id } = req.body;
const rows = await req.runQuery(
'SELECT * FROM access_tokens WHERE user_id = $1 AND item_id = $2',
[user.id, item_id],
true
);
if (rows.length === 0) {
throw new Error('access token not found');
}
const { access_token } = rows[0];
const url = config.plaid.env + '/accounts/get';
const resData = await fetch(url, {
method: 'POST',
body: JSON.stringify({
client_id: config.plaid.client_id,
secret: config.plaid.secret,
access_token: access_token
}),
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Actual Budget'
}
}).then((res) => res.json());
res.send(
JSON.stringify({
status: 'ok',
data: resData
})
);
})
);
app.post(
'/transactions',
connectDb,
handleError(async (req, res) => {
let user = await validateSubscribedUser(req, res);
if (!user) {
return;
}
let { item_id, start_date, end_date, account_id, count, offset } = req.body;
let resData;
const rows = await req.runQuery(
'SELECT * FROM access_tokens WHERE user_id = $1 AND item_id = $2 AND deleted = FALSE',
[user.id, item_id],
true
);
if (rows.length === 0) {
res.status(400);
res.send('access-token-not-found');
return;
}
const { access_token } = rows[0];
const url = config.plaid.env + '/transactions/get';
resData = await fetch(url, {
method: 'POST',
body: JSON.stringify({
client_id: config.plaid.client_id,
secret: config.plaid.secret,
access_token: access_token,
start_date: start_date,
end_date: end_date,
options: {
account_ids: [account_id],
count: count,
offset: offset
}
}),
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Actual Budget'
}
}).then((res) => res.json());
res.send(
JSON.stringify({
status: 'ok',
data: resData
})
);
})
);
app.post(
'/make-public-token',
connectDb,
handleError(async (req, res) => {
let user = await validateSubscribedUser(req, res);
if (!user) {
return;
}
let { item_id } = req.body;
const rows = await req.runQuery(
'SELECT * FROM access_tokens WHERE user_id = $1 AND item_id = $2',
[user.id, item_id],
true
);
if (rows.length === 0) {
throw new Error('access token not found');
}
const { access_token } = rows[0];
let result = await plaidClient.createLinkToken({
user: {
client_user_id: user.id
},
client_name: 'Actual',
country_codes: ['US'],
language: 'en',
access_token: access_token
});
res.send(
JSON.stringify({
status: 'ok',
data: result
})
);
})
);
module.exports.handlers = app;
module.exports.init = init;