Skip to content

Commit

Permalink
feat: first draft
Browse files Browse the repository at this point in the history
  • Loading branch information
acifani committed May 8, 2024
0 parents commit 9134a39
Show file tree
Hide file tree
Showing 10 changed files with 1,336 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Port the server will listen on
PORT=3030
# Optional auth token to use
AUTH_TOKEN=aR4nd0mT0k3n?!

ACTUAL_SERVER_URL=https://my-actual.server.dev/
ACTUAL_PASSWORD=myAc7ualP4ssword!
# This is the ID from Settings → Show advanced settings → Sync ID
ACTUAL_BUDGET_SYNC_ID=761e1721-895a-4790-acce-b2c157df1647
# How many seconds between each data pull from Actual
ACTUAL_REFRESH_INTERVAL_SECONDS=3600
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules
dist

.env*
!.env.example

data/*
!data/.gitkeep
52 changes: 52 additions & 0 deletions actual.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as api from '@actual-app/api';

export async function init() {
await api.init({
dataDir: './data',
serverURL: process.env.ACTUAL_SERVER_URL,
password: process.env.ACTUAL_PASSWORD,
});

await api.downloadBudget(process.env.ACTUAL_BUDGET_SYNC_ID);
}

export async function shutdown() {
return api.shutdown();
}

export async function getAccounts() {
return api.getAccounts();
}

export async function getTransactions(accountID: string) {
return api.getTransactions(accountID);
}

export async function getAllTransactions(offbudget?: boolean) {
const accounts = await api.getAccounts();

const accountsToFetch = accounts?.filter(
(a: any) => !a.offbudget || offbudget,
);

const transactions = [];
for (const account of accountsToFetch) {
const tx = await api.getTransactions(account.id);
transactions.push(...tx);
}

return transactions;
}

export async function getLatestBudget() {
const budgetMonths = await api.getBudgetMonths();
if (!budgetMonths?.length) {
return null;
}

return api.getBudgetMonth(budgetMonths.at(-1));
}

export async function getBudgetAtMonth(month: string) {
return api.getBudgetMonth(month);
}
19 changes: 19 additions & 0 deletions auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Request, Response, NextFunction } from 'express';

const authToken = process.env.AUTH_TOKEN;
const authEnabled = !!authToken && authToken.length > 0;

export function authMiddleware(
req: Request,
res: Response,
next: NextFunction,
) {
if (authEnabled) {
const incomingToken = req.headers['authorization'] || req.query['token'];
if (incomingToken !== authToken) {
return res.sendStatus(403);
}
}

return next();
}
Empty file added data/.gitkeep
Empty file.
85 changes: 85 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import express from 'express';
import * as actual from './actual';
import { authMiddleware } from './auth';

const app = express();

app.use(authMiddleware);

app.get('/accounts', async (_, res) => {
try {
const accounts = await actual.getAccounts();
return res.status(200).json(accounts);
} catch (e) {
console.error(e);
return res.status(500).send();
}
});

app.get('/accounts/:accountid/transactions', async (req, res) => {
try {
const transactions = await actual.getTransactions(req.params.accountid);
return res.status(200).json(transactions);
} catch (e) {
console.error(e);
return res.status(500).send();
}
});

app.get('/transactions', async (req, res) => {
try {
const offbudget = req.query['offbudget'] === 'true';
const transactions = await actual.getAllTransactions(offbudget);
return res.status(200).json(transactions);
} catch (e) {
console.error(e);
return res.status(500).send();
}
});

app.get('/budget', async (_, res) => {
try {
const budget = actual.getLatestBudget();
return res.status(200).json(budget);
} catch (e) {
console.error(e);
return res.status(500).send();
}
});

app.get('/budget/:month', async (req, res) => {
try {
const budget = await actual.getBudgetAtMonth(req.params.month);
return res.status(200).json(budget);
} catch (e) {
console.error(e);
return res.status(500).send();
}
});

// Server start
const port = process.env.PORT || 3000;
const server = app.listen(port, () => {
console.log(`Listening on port ${port}...`);
console.log('Initializing Actual DB...');
actual.init().then(() => console.log('Actual DB initialized'));
});

// Refresh Actual data at a given frequence
const intervalID = setInterval(() => {
actual.init().then(() => console.log('Actual DB refreshed'));
}, Number(process.env.ACTUAL_REFRESH_INTERVAL_SECONDS) * 1000 ?? 60_000);

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

// Graceful shutdown
function shutdown() {
clearInterval(intervalID);

console.log('Shutting down Actual DB...');
actual.shutdown().then(() => console.log('Actual DB shut down'));

console.log('Closing HTTP server...');
server.close(() => console.log('HTTP server closed'));
}
22 changes: 22 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "actual-to-csv",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"prestart": "pnpm build",
"start": "node --env-file=.env ./dist/index.js"
},
"keywords": [],
"author": "Alessandro Cifani <alessandro.cifani@gmail.com>",
"license": "MIT",
"dependencies": {
"@actual-app/api": "^6.7.1",
"express": "5.0.0-beta.3"
},
"devDependencies": {
"@types/express": "^4.17.21",
"typescript": "^5.4.5"
}
}
Loading

0 comments on commit 9134a39

Please sign in to comment.