2022-04-29 02:21:18 +00:00
|
|
|
// This app is unused right now. Maybe you could use it as a starting
|
|
|
|
// point for Plaid integration!
|
|
|
|
|
2022-03-31 17:19:08 +00:00
|
|
|
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');
|
2022-04-29 02:09:40 +00:00
|
|
|
const config = require('./load-config');
|
2022-03-31 17:19:08 +00:00
|
|
|
|
|
|
|
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;
|