Skip to content

Feat/worker - #8

Merged
Billos merged 12 commits into
mainfrom
feat/worker
Jan 18, 2026
Merged

Feat/worker#8
Billos merged 12 commits into
mainfrom
feat/worker

Conversation

@Billos

@Billos Billos commented Jan 18, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

Release Notes

  • New Features

    • Application now runs with separate server and worker services using Redis for job queue management.
    • Added webhook signature verification and API token authentication for endpoints.
  • Chores

    • Transitioned logging from console to structured logging throughout the application.
    • Updated Docker setup with explicit service configuration (Redis, Server, Worker).
    • Added new npm scripts for running development and production services independently.
  • Documentation

    • Expanded README with detailed deployment guidance for multiple setup modes.

✏️ Tip: You can customize this high-level summary in your review settings.

Billos added 12 commits January 18, 2026 14:55
Moves job delay logic to a dedicated module for better organization and reusability.

This change enhances the queue processing by centralizing the delay calculation and making it easier to adjust and maintain. It also ensures consistency in how job delays are handled across different parts of the application.
Implements middleware to parse the request body, allowing raw access to the body as a string and parsing it to JSON.
Removes the `/` endpoint, as it is no longer needed with the introduction of the worker architecture.
Removes the original server entrypoint (src/index.ts) and related scripts.

This is done to shift the application architecture to a worker-based model.
The original index.ts file contained the Express server setup which is now handled by a dedicated server entrypoint (src/server.ts).
Updates the webhook route from "/transaction" to "/webhook" to align with the worker service.
Ensures that duplicate messages are not sent to the queue for transactions.
If a message already exists for a given transaction, the function now returns early
and logs an info message instead of sending a duplicate.

This change prevents redundant notifications and ensures that the queue processes only
unique transaction-related messages.
@Billos Billos self-assigned this Jan 18, 2026
@coderabbitai

coderabbitai Bot commented Jan 18, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

The PR refactors the application from a monolithic architecture into a separated server-worker model using Redis for job queueing. It introduces new Docker services, establishes token-based API security, adds structured logging via pino, and splits entry points into server and worker processes with corresponding npm scripts.

Changes

Cohort / File(s) Summary
Infrastructure & Container Configuration
.env.default, Dockerfile, docker-compose.yml, docker-compose.dev.yml, package.json
New environment variables (FIREFLY_III_WEBHOOK_SECRET, API_TOKEN) added. Docker-compose refactored to include Redis service and separate server/worker services with command variants. Dockerfile adds default CMD. Package.json splits dev/start scripts (dev:server, dev:worker, start:server, start:worker) and adds pino/pino-pretty dependencies.
Documentation
README.md
Expanded with Docker Compose setup instructions, service descriptions, port mappings, production/development/standalone deployment modes, and updated Redis role clarification from caching to job queue management.
Architecture & Entry Points
src/index.ts, src/server.ts, src/worker.ts
Removed monolithic index.ts bootstrap. Introduced src/server.ts (Express app with routes and middleware) and src/worker.ts (worker initialization via BullMQ). Separates server and worker lifecycle.
Queue Infrastructure
src/queues/index.ts, src/queues/delay.ts, src/queues/constants.ts, src/queues/unbudgetedTransactions.ts, src/queues/uncategorizedTransactions.ts, src/queues/updateAutomaticBudgets.ts
Extracted getJobDelay into new delay.ts module. Exported JOB_DELAYS constant. Refactored queue initialization to support worker bootstrap via initializeWorker. Updated job delay values. Added init callbacks to job definitions. Enhanced logging and URL construction with api_token query parameter.
Security & Middleware
src/utils/middleware.ts, src/utils/tokenMiddleware.ts, src/utils/webhookSecret.ts
New ParseBodyMiddleware to capture rawBody for request parsing. New TokenMiddleware for API token validation via query parameter. New verifyWebhookMiddleware for HMAC-SHA3-256 signature verification using env.fireflyWebhookSecret.
Webhook & Endpoints
src/endpoints/webhook.ts, src/endpoints/updateAutomaticBudgets.ts, src/endpoints/settingBudgetForTransaction.ts, src/endpoints/settingCategoryForTransaction.ts
Updated webhook.ts to use new delay import path and log job scheduling. Endpoints updated to use TokenMiddleware for security. Logging refactored from console to pino across all endpoints.
Controllers & Logging
src/controllers/linkPaypalTransactions.ts, src/controllers/reviewBudgetLimit.ts, src/controllers/updateBillsBudgetLimit.ts, src/controllers/updateLeftoversBudget.ts
Replaced console.log with structured pino logging. Updated budget tracking logic to use getSumWithoutLeftovers helper and improved logging around budget calculations and limits.
Modules & Configuration
src/modules/transactionHandler/transactionHandler.ts, src/config.ts, src/types/core/request.ts, src/utils/date.ts
Added pino logging to transaction handler error paths. Extended config.ts with fireflyWebhookSecret and apiToken env variables. Updated catch parameter naming conventions. Commented out console output in date utility.
Linting
eslint.config.mjs
Added "no-console": "error" rule to enforce structured logging.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Server as Server<br/>(HTTP)
    participant Redis as Redis<br/>(Queue)
    participant Worker
    participant FireflyAPI as Firefly API

    Client->>Server: POST /webhook<br/>(with signature)
    Server->>Server: Verify webhook signature
    Server->>Redis: Enqueue job<br/>(e.g., unbudgeted_transactions)
    Server-->>Client: 200 OK

    Worker->>Redis: Poll for jobs
    Worker->>FireflyAPI: Fetch transaction details
    Worker->>FireflyAPI: Get budget limits
    Worker->>FireflyAPI: Create/Update budget assignments
    Worker->>Redis: Mark job complete
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

  • Fix/log delay #6: Modifies job delay handling and webhook logging that align with the new delay.ts module and structured logging in webhook.ts.
  • Fix(webhook): Corrects webhook job execution #3: Directly related to queue API changes, including the split of transactionJobDefinitions from jobDefinitions and webhook job enqueuing logic.

Poem

🐰 Behold! The monolith doth split in two,
A server here, a worker there—both new!
With Redis queues and logs so bright and clear,
This rabbit hops with joy to see it here! 🎉

✨ Finishing touches
  • 📝 Generate docstrings

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 markdownlint-cli2 (0.18.1)
README.md

markdownlint-cli2 v0.18.1 (markdownlint v0.38.0)
Finding: README.md
Linting: 1 file(s)
Summary: 0 error(s)
Error: EACCES: permission denied, open '/markdownlint-cli2-results.json'
at async open (node:internal/fs/promises:640:25)
at async Object.writeFile (node:internal/fs/promises:1214:14)
at async Promise.all (index 0)
at async outputSummary (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:877:5)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:1053:25)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:12:22 {
errno: -13,
code: 'EACCES',
syscall: 'open',
path: '/markdownlint-cli2-results.json'
}


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Billos
Billos merged commit d8b5326 into main Jan 18, 2026
3 of 4 checks passed
Comment thread src/server.ts

app.get("/transaction/:transactionId/budget/:budget_id", TokenMiddleware, settingBudgetForTransaction)
app.get("/transaction/:transactionId/category/:category_id", TokenMiddleware, settingCategoryForTransaction)
app.post("/webhook", verifyWebhookMiddleware, webhook)

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI 7 months ago

In general, to fix missing rate limiting on a sensitive or potentially expensive route, you introduce a rate‑limiting middleware (e.g. using express-rate-limit) and apply it to that specific route or to the entire app, depending on your requirements. The limiter should enforce a reasonable maximum number of requests per IP (or other key) over a time window, returning 429 responses when exceeded. This mitigates simple denial‑of‑service attempts that rely on flooding the endpoint.

For this codebase, the minimal, targeted fix is to add express-rate-limit and apply a limiter only to the /webhook route, leaving the other routes unchanged. Concretely in src/server.ts, we will:

  • Import express-rate-limit near the top of the file (without changing existing imports).
  • Define a webhookLimiter constant after creating the app or before defining the routes. This limiter will set a window and a maximum number of requests per IP.
  • Attach webhookLimiter to the /webhook route by adding it to the middleware chain: app.post("/webhook", webhookLimiter, verifyWebhookMiddleware, webhook).
    No existing functionality is removed; we are only adding a new middleware layer in front of the current ones.
Suggested changeset 2
src/server.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/server.ts b/src/server.ts
--- a/src/server.ts
+++ b/src/server.ts
@@ -1,5 +1,6 @@
 import express from "express"
 import pino from "pino"
+import rateLimit from "express-rate-limit"
 
 import { env } from "./config"
 import { settingBudgetForTransaction } from "./endpoints/settingBudgetForTransaction"
@@ -12,11 +13,16 @@
 const logger = pino()
 const app = express()
 
+const webhookLimiter = rateLimit({
+  windowMs: 15 * 60 * 1000, // 15 minutes
+  max: 100, // limit each IP to 100 webhook requests per window
+})
+
 app.use(ParseBodyMiddleware)
 
 app.get("/transaction/:transactionId/budget/:budget_id", TokenMiddleware, settingBudgetForTransaction)
 app.get("/transaction/:transactionId/category/:category_id", TokenMiddleware, settingCategoryForTransaction)
-app.post("/webhook", verifyWebhookMiddleware, webhook)
+app.post("/webhook", webhookLimiter, verifyWebhookMiddleware, webhook)
 
 async function startServer() {
   try {
EOF
@@ -1,5 +1,6 @@
import express from "express"
import pino from "pino"
import rateLimit from "express-rate-limit"

import { env } from "./config"
import { settingBudgetForTransaction } from "./endpoints/settingBudgetForTransaction"
@@ -12,11 +13,16 @@
const logger = pino()
const app = express()

const webhookLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 webhook requests per window
})

app.use(ParseBodyMiddleware)

app.get("/transaction/:transactionId/budget/:budget_id", TokenMiddleware, settingBudgetForTransaction)
app.get("/transaction/:transactionId/category/:category_id", TokenMiddleware, settingCategoryForTransaction)
app.post("/webhook", verifyWebhookMiddleware, webhook)
app.post("/webhook", webhookLimiter, verifyWebhookMiddleware, webhook)

async function startServer() {
try {
package.json
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -30,7 +30,8 @@
     "express": "^5.1.0",
     "luxon": "^3.5.0",
     "pino": "^10.2.0",
-    "pino-pretty": "^13.1.3"
+    "pino-pretty": "^13.1.3",
+    "express-rate-limit": "^8.2.1"
   },
   "devDependencies": {
     "@codedependant/semantic-release-docker": "^5.0.3",
EOF
@@ -30,7 +30,8 @@
"express": "^5.1.0",
"luxon": "^3.5.0",
"pino": "^10.2.0",
"pino-pretty": "^13.1.3"
"pino-pretty": "^13.1.3",
"express-rate-limit": "^8.2.1"
},
"devDependencies": {
"@codedependant/semantic-release-docker": "^5.0.3",
This fix introduces these dependencies
Package Version Security advisories
express-rate-limit (npm) 8.2.1 None
Copilot is powered by AI and may make mistakes. Always verify output.

export async function TokenMiddleware(req: Request, res: Response, next: NextFunction): Promise<void> {
// Token is passed in the query parameters as ?api_token=...TOKEN
const token = req.query.api_token as string | undefined

Check warning

Code scanning / CodeQL

Sensitive data read from GET request Medium

Route handler
for GET requests uses query parameter as sensitive data.

Copilot Autofix

AI 7 months ago

In general, sensitive credentials like API tokens should not be transmitted in URLs (query strings). Instead, they should be sent in HTTP headers (for example, Authorization or a custom header) or, less ideally, in the body of POST/PUT requests. The middleware should validate tokens from these safer locations and stop encouraging the use of query parameters.

The best way to fix this code without changing existing functionality too drastically is to (1) stop reading the token from req.query.api_token, and (2) read it from a commonly used secure location, such as a bearer token in the Authorization header, while optionally also checking req.body.api_token for non‑GET requests. To avoid breaking callers too hard, we can support multiple inputs but prioritize headers and bodies, and we should remove the explicit guidance in the comment that tells users to put tokens in the query string. Within src/utils/tokenMiddleware.ts, change line 10 so that it derives token from req.headers.authorization (parsing a Bearer <token> scheme) and, as a fallback, from req.body.api_token if present. This does not require any new imports; Express already exposes req.headers and req.body (assuming body‑parsing middleware is configured elsewhere).

Concretely:

  • Update the comment on line 9 to no longer recommend query parameters; instead, document the new, safer mechanism.
  • Replace const token = req.query.api_token as string | undefined with logic that:
    • Reads req.headers.authorization and, if it starts with Bearer (case-insensitive), extracts the token value.
    • If no header token is present, attempts to read req.body.api_token (typed as any/unknown then cast to string | undefined).
  • Keep the rest of the middleware unchanged so that the validation and responses behave the same, only the token source changes.
Suggested changeset 1
src/utils/tokenMiddleware.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/utils/tokenMiddleware.ts b/src/utils/tokenMiddleware.ts
--- a/src/utils/tokenMiddleware.ts
+++ b/src/utils/tokenMiddleware.ts
@@ -6,8 +6,16 @@
 const logger = pino()
 
 export async function TokenMiddleware(req: Request, res: Response, next: NextFunction): Promise<void> {
-  // Token is passed in the query parameters as ?api_token=...TOKEN
-  const token = req.query.api_token as string | undefined
+  // Token should be provided via the Authorization header as "Bearer <token>"
+  let token: string | undefined
+
+  const authHeader = req.headers["authorization"]
+  if (typeof authHeader === "string" && authHeader.toLowerCase().startsWith("bearer ")) {
+    token = authHeader.slice(7).trim()
+  } else if (req.body && typeof (req.body as any).api_token === "string") {
+    token = (req.body as any).api_token
+  }
+
   logger.info("Verifying API token")
 
   if (!token || token !== env.apiToken) {
EOF
@@ -6,8 +6,16 @@
const logger = pino()

export async function TokenMiddleware(req: Request, res: Response, next: NextFunction): Promise<void> {
// Token is passed in the query parameters as ?api_token=...TOKEN
const token = req.query.api_token as string | undefined
// Token should be provided via the Authorization header as "Bearer <token>"
let token: string | undefined

const authHeader = req.headers["authorization"]
if (typeof authHeader === "string" && authHeader.toLowerCase().startsWith("bearer ")) {
token = authHeader.slice(7).trim()
} else if (req.body && typeof (req.body as any).api_token === "string") {
token = (req.body as any).api_token
}

logger.info("Verifying API token")

if (!token || token !== env.apiToken) {
Copilot is powered by AI and may make mistakes. Always verify output.
@Billos
Billos deleted the feat/worker branch January 19, 2026 00:12
This was referenced Jan 22, 2026
This was referenced Jul 5, 2026
Merged
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants