From 36d2278e791c80959c8820d57234d3246d6ce3ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elisi=C3=A1rio=20Couto?= Date: Fri, 23 Dec 2022 19:00:20 +0000 Subject: [PATCH] Add bootstrap script and documentation. --- CONTRIBUTING.md | 71 ++++++----------------------------- LICENSE | 21 +++++++++++ README.md | 48 +++++++++++++++++++++++- package-lock.json | 4 +- package.json | 7 ++-- scripts/bootstrap.sh | 84 ++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 28 +++++++------- wrangler.toml.template | 4 +- 8 files changed, 186 insertions(+), 81 deletions(-) create mode 100644 LICENSE create mode 100755 scripts/bootstrap.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 641ff3a..d653d69 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,66 +1,19 @@ # Getting started -1. Create a `.env` file in the root of the repository with the Cloudflare API Token and Account ID: - ``` - CLOUDFLARE_API_TOKEN= - CLOUDFLARE_ACCOUNT_ID= - ``` - -2. Create a `.dev.vars` file in the root of the repository with the following content: - ``` - NORDIGEN_SECRET_ID= - NORDIGEN_SECRET_KEY= - NORDIGEN_ACCOUNT_ID= - NOTIFY_PATTERN= - DISCORD_URL= - ``` - -3. Install the dependencies: +To run the project locally: ```shell npm i - ``` - -4. Create a `wrangler.toml` from the template: - ```shell - cp wrangler.toml.template wrangler.toml - ``` - -5. Create a KV for bankman: - ```shell - wrangler kv:namespace create "BANKMAN_KV" - ``` - -6. Copy the KV ID from previous command to your wrangler.toml: - ```shell - sed -i wrangler.toml "s/KV_ID/$(wrangler kv:namespace list | grep BANKMAN_KV -B1 | head -n1 | cut -d'"' -f4)/" - ``` - -7. Create a D1 for bankman: - ```shell - wrangler d1 create bankmandb - ``` - -8. Copy the database ID from previous command to your wrangler.toml: - ```shell - sed -i wrangler.toml "s/DATABASE_ID/$(wrangler d1 list | grep bankman | awk '{print $2}')/" - ``` - -9. Run database migrations: - ```shell - wrangler d1 migrations apply bankman - ``` - -10. Push secrets for this worker: - ```shell - wrangler secret:bulk <(cat .dev.vars | sed 's/"//g' | sed "s/^/\"/g" | sed "s/=/\":\"/g" | sed "s/$/\",/g" | tr -d '\n' | sed "s/^/{/" | tr -s ',' | sed "s/,$/}/") - ``` - -11. Run the project locally: - ```shell npm start ``` -12. The project runs every minute, but if you want to manually trigger an event, run the following: - ```shell - curl "http://localhost:8787/cdn-cgi/mf/scheduled" - ``` +You also need to run the database migrations: +```shell +for file in ./migrations/*.sql; do + npx wrangler d1 execute bankmandb --local --file="$file" +done +``` + +The project runs every hour by default, but if you want to manually trigger an event, run the following: +```shell +curl "http://localhost:8787/cdn-cgi/mf/scheduled" +``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ffadd77 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Meltway Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 3c62011..a528bf6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,51 @@ -# Bankman +# $ bankman A cron job running serverless on Cloudflare Workers. This project uses Nordigen to connect to a bank account and searches for a specific transaction regex, alerting on Discord when this happens. + +## Pre-Requisites +#### 1. Cloudflare Account +This project runs on Cloudflare Workers. You need a Cloudflare API token. + +Use the `Edit Cloudflare Workers` template and add a new rule: +``` +Account | D1 | Edit +``` + +#### 2. Nordigen Account +Nordigen connects to your Open Banking-compatible bank account. + +Sign up and follow the quickstart guide: https://nordigen.com/en/account_information_documenation/integration/quickstart_guide/ + +Save the account ID from step 5, you will need it later. + +#### 3. Discord Webhook +In order to send notifications, we currently only support Discord Webhooks. + +## Getting started + +1. Create a `.env` file in the root of the repository with the Cloudflare API Token and Account ID: + ``` + CLOUDFLARE_API_TOKEN= + CLOUDFLARE_ACCOUNT_ID= + ``` + +2. Create a `.dev.vars` file in the root of the repository with the following content: + ``` + NORDIGEN_SECRET_ID= + NORDIGEN_SECRET_KEY= + NORDIGEN_ACCOUNT_ID= + NOTIFY_PATTERN= + DISCORD_URL= + ``` + +3. Install the dependencies: + ```shell + npm i + ``` + +4. Run the bootstrap script to create all the resources and deploy: + ```shell + npm run bootstrap + ``` diff --git a/package-lock.json b/package-lock.json index 127e115..2252605 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bankman", - "version": "0.0.0", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bankman", - "version": "0.0.0", + "version": "0.1.0", "devDependencies": { "@cloudflare/workers-types": "^4.20221111.1", "better-sqlite3": "^7.6.2", diff --git a/package.json b/package.json index 1f96547..c28cdca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bankman", - "version": "0.0.0", + "version": "0.1.0", "devDependencies": { "@cloudflare/workers-types": "^4.20221111.1", "better-sqlite3": "^7.6.2", @@ -9,7 +9,8 @@ }, "private": true, "scripts": { - "start": "wrangler dev --local", - "deploy": "wrangler publish" + "bootstrap": "./scripts/bootstrap.sh", + "deploy": "wrangler publish --name bankman", + "start": "wrangler dev --local" } } diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100755 index 0000000..30b410f --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +set -euf -o pipefail + +NO_D1_WARNING=true +export NO_D1_WARNING + +export RED="\033[1;31m" +export GREEN="\033[1;32m" +export YELLOW="\033[1;33m" +export BLUE="\033[1;34m" +export PURPLE="\033[1;35m" +export CYAN="\033[1;36m" +export GREY="\033[0;37m" +export RESET="\033[m" + +check_command() { + if ! command -v "$1" &> /dev/null + then + echo "$1 could not be found." + echo "Exiting." + exit 1 + fi +} + +print_error() { + echo -e "${RED}$1${RESET}" +} + +print_info() { + echo -e "${CYAN}$1${RESET}" +} +print_success() { + echo -e "${GREEN}$1${RESET}" +} + +check_command jq +check_command npx +check_command envsubst +check_command sed +check_command tr +check_command grep +check_command awk + +if ! npm exec --no -- wrangler --version &> /dev/null +then + # shellcheck disable=SC2016 + print_error "wrangler could not be found. Did you run \`npm install\` ?" + print_error "Exiting." + exit 1 +fi + +cp wrangler.toml.template wrangler.toml +print_info "> Creating KV namespace" +npx wrangler kv:namespace create "KV" +# shellcheck disable=SC2034 +KV_ID=$(npx wrangler kv:namespace list | jq -r '.[] | select(.title == "bankman-KV") | .id') +export KV_ID + +print_info "> Creating D1 database" +npx wrangler d1 create bankmandb +# shellcheck disable=SC2034 +DATABASE_ID=$(npx wrangler d1 list | grep bankman | awk '{print $2}') +export DATABASE_ID + +print_info "> Creating wrangler.toml" +envsubst < wrangler.toml.template > wrangler.toml + +print_info "> Waiting for DB to be ready" +sleep 10 + +print_info "> Applying DB migrations" +# Remove hacky way of disabling prompt after wrangler has a -y, --skip-confirmation flag +CF_PAGES=1 npx wrangler d1 migrations apply bankmandb 2> /dev/null + +print_info "> Publishing Cloudflare Worker" +wrangler publish --name bankman + +print_info "> Updating secrets" +sed 's/"//g' .dev.vars | sed "s/^/\"/g" | sed "s/=/\":\"/g" | sed "s/$/\",/g" | tr -d '\n' | sed "s/^/{/" | tr -s ',' | sed "s/,$/}/" > tmp +wrangler secret:bulk tmp +rm -f tmp + +print_success "> Bankman deployed" diff --git a/src/index.ts b/src/index.ts index 1cacace..34a1d0c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,12 +17,12 @@ export interface Env { NORDIGEN_AGREEMENT_ID: string; NOTIFY_PATTERN: string; DISCORD_URL: string; - BANKMAN_KV: KVNamespace; + KV: KVNamespace; DB: D1Database; } // one day in milliseconds -const ONE_DAY_MS = 1000 * 60 * 60 * 24; +const ONE_DAY_MS = 1000 * 60 * 60 * 24; // days before today to fetch transactions from const DAYS_TO_FETCH = 2; // notify expiration once a day under X days @@ -115,7 +115,7 @@ async function fetchNordigenAgreementExpiration( const byId = id && id !== ""; const url = byId ? - NORDIGEN_HOST + `/api/v2/agreements/enduser/${id}/`: + NORDIGEN_HOST + `/api/v2/agreements/enduser/${id}/` : NORDIGEN_HOST + `/api/v2/agreements/enduser/?limit=1`; const resp = await fetch(url, { @@ -143,7 +143,7 @@ async function fetchNordigenAgreementExpiration( const expiryDate = (new Date(agreement.accepted)).getTime() + ONE_DAY_MS * agreement.access_valid_for_days; - return (expiryDate - Date.now())/ONE_DAY_MS + return (expiryDate - Date.now()) / ONE_DAY_MS } async function storeBankTransactions( @@ -237,11 +237,11 @@ async function doExecute(env: Env) { // check agreement expiration and notify if needed const expirationDays = await fetchNordigenAgreementExpiration(token, env.NORDIGEN_AGREEMENT_ID); if (expirationDays <= NOTIFY_EXPIRATION_DAYS) { - const hasNotifiedToday = await checkNotifiedToday(env.BANKMAN_KV); + const hasNotifiedToday = await checkNotifiedToday(env.KV); if (!hasNotifiedToday) { const message = `End user agreement expires in ${expirationDays} days.`; await notifyDiscord(env.DISCORD_URL, message); - await markNotifiedToday(env.BANKMAN_KV); + await markNotifiedToday(env.KV); } } @@ -305,12 +305,12 @@ export default { }, // Uncomment lines below to allow invoking with fetch. - // async fetch( - // request: RequestInit, - // env: Env, - // context: ExecutionContext, - // ): Promise { - // await execute(env); - // return Response.json({status: "ok"}) - // } + async fetch( + request: RequestInit, + env: Env, + context: ExecutionContext, + ): Promise { + await execute(env); + return Response.json({ status: "ok" }) + } }; diff --git a/wrangler.toml.template b/wrangler.toml.template index 1beb24a..80c8dc4 100644 --- a/wrangler.toml.template +++ b/wrangler.toml.template @@ -8,8 +8,8 @@ crons = [ "0 * * * *" ] [[ d1_databases ]] binding = "DB" database_name = "bankmandb" -database_id = "DATABASE_ID" +database_id = "$DATABASE_ID" kv_namespaces = [ - { binding = "BANKMAN_KV", id = "KV_ID" } + { binding = "KV", id = "$KV_ID" } ]