Compare commits

10 Commits

Author SHA1 Message Date
Knee Cola
7a3a02bd6d chore: bump mailgun-webhook version to 1.0.1 and update lockfiles
- Bump mailgun-webhook-service version from 1.0.0 to 1.0.1
- Add mailgun-webhook to root workspace in package-lock.json
- Update all lockfiles to reflect workspace changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 22:16:18 +01:00
Knee Cola
3d02654510 feat: add webhook signature verification and fix security issues
Security Improvements:
- Add HMAC-SHA256 signature verification for MailGun webhooks
- Remove hardcoded signing key fallback, require env variable
- Add proper payload structure validation before processing

API Changes:
- New types: MailgunWebhookPayload, MailgunWebhookPayloadSignature
- New type guard: isMailgunWebhookPayload()
- Returns 401 for invalid signatures, 400 for malformed payloads

Configuration:
- Add MAILGUN_WEBHOOK_SIGNING_KEY to both docker-compose files
- Service fails fast on startup if signing key not configured

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 22:13:09 +01:00
Knee Cola
0faac8e392 chore: update mailgun-webhook service URL to mailgun-webhook.rezije.app
- Change from webhook.rezije.app to mailgun-webhook.rezije.app for clarity
- Update both standalone and swarm configurations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 21:18:18 +01:00
Knee Cola
f9f33a2b45 feat: add mailgun-webhook service to docker-compose configurations
- Add mailgun-webhook service to both standalone and swarm deployments
- Configure service with Prometheus monitoring and debug logging
- Route traffic through Traefik at webhook.rezije.app
- Use version-controlled image with MAILGUN_WEBHOOK_VERSION variable

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 21:17:08 +01:00
Knee Cola
ccc690c369 chore: upgrade Node.js versions and improve Docker security
- Upgrade email-worker from Node 18 to Node 20
- Update distroless images to nodejs20-debian12:nonroot for both services
- Improves security by running containers as nonroot user

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 21:12:20 +01:00
d2725261d5 chore: add mailgun-webhook workspace to monorepo
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 16:08:50 +01:00
f169e2c4ba renamed script file 2026-01-07 16:08:02 +01:00
c72a06e34e feat: add CI/CD script for building Docker images across workspaces
Add ci-build-docker-image.sh script to automate Docker image builds in CI/CD pipelines. Script iterates through configured workspaces and builds images using --auto-version and --auto-push flags. Currently configured for mailgun-webhook, easily extensible for additional workspaces.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 13:16:28 +01:00
9d6507c3ae feat: add --auto-version flag with registry check to build-image.sh
Add --auto-version flag to automatically use version from package.json and check if image already exists in registry before building. If image exists, script exits to prevent duplicate versions. Rename --autopush to --auto-push for consistent flag naming.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 13:09:30 +01:00
6cf9b312c0 feat: add --autopush flag to build-image.sh for non-interactive builds
Add optional --autopush parameter to skip the interactive push prompt and automatically push built images to registry. Useful for CI/CD pipelines and automated builds.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 12:51:41 +01:00
12 changed files with 9099 additions and 7771 deletions

36
ci-build.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
# CI/CD script to build and push Docker images for workspace projects
# Uses version from package.json and automatically pushes to registry
set -e # Exit on error
# List of workspaces to build
WORKSPACES=(
"mailgun-webhook"
)
printf "\n=== CI/CD Docker Image Build ===\n"
printf "Building %d workspace(s)\n\n" "${#WORKSPACES[@]}"
for WORKSPACE_DIR in "${WORKSPACES[@]}"; do
printf "\n--- Building workspace: %s ---\n\n" "$WORKSPACE_DIR"
if [ ! -d "$WORKSPACE_DIR" ]; then
printf "\nERROR: Directory '%s' does not exist. Skipping.\n\n" "$WORKSPACE_DIR"
continue
fi
if [ ! -f "$WORKSPACE_DIR/build-image.sh" ]; then
printf "\nERROR: build-image.sh not found in '%s'. Skipping.\n\n" "$WORKSPACE_DIR"
continue
fi
cd "$WORKSPACE_DIR"
./build-image.sh --auto-version --auto-push
cd ..
printf "\n--- Completed: %s ---\n" "$WORKSPACE_DIR"
done
printf "\n=== All builds completed successfully! ===\n\n"

View File

@@ -79,3 +79,22 @@ services:
- traefik.http.routers.mongo-express.entrypoints=http
- traefik.http.routers.mongo-express.rule=Host(`mongo.rezije.app`)
mailgun-webhook:
image: registry.budakova.org/mailgun-webhook-service:${MAILGUN_WEBHOOK_VERSION:-latest}
networks:
- traefik-network
environment:
PORT: 3000
PROMETHEUS_APP_LABEL: mailgun-webhook-service
PROMETHEUS_HISTOGRAM_BUCKETS: 0.1,0.5,1,5,10
DEBUG: server:*,app:*
MAILGUN_WEBHOOK_SIGNING_KEY: ${MAILGUN_WEBHOOK_SIGNING_KEY}
container_name: evidencija-rezija__mailgun-webhook
restart: unless-stopped
labels:
- traefik.enable=true
- traefik.docker.network=traefik-network
- traefik.http.services.mailgun-webhook.loadbalancer.server.port=3000
- traefik.http.routers.mailgun-webhook.entrypoints=http
- traefik.http.routers.mailgun-webhook.rule=Host(`mailgun-webhook.rezije.app`)

View File

@@ -79,3 +79,25 @@ services:
- traefik.http.routers.mongo-express.entrypoints=http
- traefik.http.routers.mongo-express.rule=Host(`mongo.rezije.app`)
mailgun-webhook:
image: registry.budakova.org/mailgun-webhook-service:${MAILGUN_WEBHOOK_VERSION:-latest}
networks:
- traefik-network
environment:
PORT: 3000
PROMETHEUS_APP_LABEL: mailgun-webhook-service
PROMETHEUS_HISTOGRAM_BUCKETS: 0.1,0.5,1,5,10
DEBUG: server:*,app:*
MAILGUN_WEBHOOK_SIGNING_KEY: ${MAILGUN_WEBHOOK_SIGNING_KEY}
deploy:
restart_policy:
condition: any
delay: 5s
max_attempts: 0
labels:
- traefik.enable=true
- traefik.docker.network=traefik-network
- traefik.http.services.mailgun-webhook.loadbalancer.server.port=3000
- traefik.http.routers.mailgun-webhook.entrypoints=http
- traefik.http.routers.mailgun-webhook.rule=Host(`mailgun-webhook.rezije.app`)

View File

@@ -1,7 +1,7 @@
#--------------------------------------------
# Stage: building TypeScript
#--------------------------------------------
FROM node:18 as build-stage
FROM node:20 as build-stage
ENV WORKDIR=/app
WORKDIR /app
@@ -30,7 +30,7 @@ RUN npm i --only=production && npm cache clean --force
#--------------------------------------------
# Stage: priprema finalnog image-a
#--------------------------------------------
FROM gcr.io/distroless/nodejs:18 as assembly-stage
FROM gcr.io/distroless/nodejs20-debian12:nonroot as assembly-stage
WORKDIR /app

View File

@@ -30,7 +30,7 @@ RUN npm i --omit=dev && npm cache clean --force
#--------------------------------------------
# Stage: preparing final image
#--------------------------------------------
FROM gcr.io/distroless/nodejs:20 AS assembly-stage
FROM gcr.io/distroless/nodejs20-debian12:nonroot AS assembly-stage
WORKDIR /app

View File

@@ -1,23 +1,64 @@
#!/bin/bash
if [ "$1" == "" ] ; then
# Parse flags
AUTO_VERSION=false
AUTO_PUSH=false
IMAGE_VERSION=""
for arg in "$@"; do
case $arg in
--auto-version)
AUTO_VERSION=true
;;
--auto-push)
AUTO_PUSH=true
;;
*)
if [ "$IMAGE_VERSION" == "" ] && [[ ! "$arg" =~ ^-- ]]; then
IMAGE_VERSION=$arg
fi
;;
esac
done
# Determine version
if [ "$AUTO_VERSION" = true ]; then
IMAGE_VERSION=$(node -p "require('./package.json').version")
printf "\nAuto-version enabled. Using version from package.json: %s\n" "$IMAGE_VERSION"
elif [ "$IMAGE_VERSION" == "" ]; then
printf "\nYou did not specify the Docker image version to build"
printf "\n\nSyntax:\n\n build-image.sh 1.0.0\n\n"
printf "\n\nSyntax:\n\n build-image.sh <version> [--auto-push]"
printf "\n build-image.sh --auto-version [--auto-push]\n\n"
exit 1
fi
read -p "BUILD: Push new image to registry [y/n]? " -n 1 -r
echo # (optional) move to a new line
REGISTRY_URL="registry.budakova.org"
IMAGE_NAME=$(node -p "require('./package.json').name")
IMAGE_TAG=$REGISTRY_URL/$IMAGE_NAME:$IMAGE_VERSION
PUSH_IMAGE="$REPLY"
# Check if image already exists in registry (only when using auto-version)
if [ "$AUTO_VERSION" = true ]; then
printf "\nChecking if image %s already exists in registry...\n" "$IMAGE_TAG"
if docker manifest inspect $IMAGE_TAG > /dev/null 2>&1; then
printf "\nERROR: Image %s already exists in registry.\n" "$IMAGE_TAG"
printf "Please update the version in package.json before building.\n\n"
exit 1
fi
printf "Image does not exist in registry. Proceeding with build.\n"
fi
# Check for push preference
if [ "$AUTO_PUSH" = true ]; then
PUSH_IMAGE="y"
printf "\nAuto-push enabled. Image will be pushed to registry.\n"
else
read -p "BUILD: Push new image to registry [y/n]? " -n 1 -r
echo # (optional) move to a new line
PUSH_IMAGE="$REPLY"
fi
printf "\nBUILD START ...\n\n"
REGISTRY_URL="registry.budakova.org"
IMAGE_NAME=$(node -p "require('./package.json').name")
IMAGE_VERSION=$1
IMAGE_TAG=$REGISTRY_URL/$IMAGE_NAME:$IMAGE_VERSION
docker build . -t $IMAGE_TAG
if [[ "$PUSH_IMAGE" =~ ^[Yy]$ ]]

View File

@@ -1,12 +1,12 @@
{
"name": "mailgun-webhook-service",
"version": "1.0.0",
"version": "1.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mailgun-webhook-service",
"version": "1.0.0",
"version": "1.0.1",
"license": "ISC",
"dependencies": {
"debug": "^2.6.9",

View File

@@ -1,6 +1,6 @@
{
"name": "mailgun-webhook-service",
"version": "1.0.0",
"version": "1.0.1",
"description": "MailGun webhook receiver service for logging email event notifications",
"main": "entry.ts",
"scripts": {

View File

@@ -9,9 +9,31 @@
import { Router, Request, Response, NextFunction } from 'express';
import createError from 'http-errors';
import { AppLocals } from '../types/AppLocals';
import { isValidMailgunEvent, MailgunWebhookEvent } from '../types/MailgunWebhookEvent';
import { isMailgunWebhookPayload, isValidMailgunEvent, MailgunWebhookEvent, MailgunWebhookPayloadSignature } from '../types/MailgunWebhookEvent';
import { successfulRequestCounter } from '../lib/metricsCounters';
import { logInfo, logError, logWarn } from '../lib/logger';
import crypto from 'crypto';
const WEBHOOK_SIGNING_KEY = process.env.MAILGUN_WEBHOOK_SIGNING_KEY;
if (!WEBHOOK_SIGNING_KEY) {
logError('Configuration Error', 'MAILGUN_WEBHOOK_SIGNING_KEY environment variable is not set');
throw new Error('MAILGUN_WEBHOOK_SIGNING_KEY environment variable is required');
}
/**
* Verifies the MailGun webhook signature
* @param param0 - Object containing signingKey, timestamp, token, and signature
* @returns boolean indicating if the signature is valid
*/
const verifySignature = ({ timestamp, token, signature }: MailgunWebhookPayloadSignature) => {
const encodedToken = crypto
.createHmac('sha256', WEBHOOK_SIGNING_KEY)
.update(timestamp.concat(token))
.digest('hex')
return (encodedToken === signature)
}
/**
* Formats a Unix timestamp into a human-readable date string
@@ -117,7 +139,16 @@ const logWebhookEvent = (eventData: MailgunWebhookEvent): void => {
*/
export const webhookRequestHandler = async (req: Request, res: Response, next: NextFunction) => {
try {
const eventData = req.body as any;
const payload = req.body as any;
if(!isMailgunWebhookPayload(payload)) {
logWarn('Invalid webhook payload structure', JSON.stringify(payload));
next(createError(400, 'Invalid request format: payload structure is incorrect'));
return;
}
const { signature, "event-data": eventData } = payload;
// Validate that we have the minimum required fields
if (!isValidMailgunEvent(eventData)) {
@@ -126,6 +157,19 @@ export const webhookRequestHandler = async (req: Request, res: Response, next: N
return;
}
// Verify webhook signature
const isValidSignature = verifySignature({
timestamp: signature.timestamp,
token: signature.token,
signature: signature.signature
});
if(!isValidSignature) {
logWarn('Invalid webhook signature', JSON.stringify(signature));
next(createError(401, 'Unauthorized: invalid webhook signature'));
return;
}
// Log the webhook event to console
logWebhookEvent(eventData as MailgunWebhookEvent);

View File

@@ -14,6 +14,17 @@ export type MailgunEventType =
| 'complained'
| 'unsubscribed';
export interface MailgunWebhookPayload {
signature: MailgunWebhookPayloadSignature;
"event-data": MailgunWebhookEvent;
}
export interface MailgunWebhookPayloadSignature {
token: string;
timestamp: string;
signature: string;
}
/**
* Base interface for all MailGun webhook events
* Contains fields common to all event types
@@ -163,3 +174,15 @@ export function isValidMailgunEvent(data: any): data is MailgunWebhookEventBase
typeof data.recipient === 'string'
);
}
/**
* Type guard to check if data is a MailgunWebhookPayload
*/
export function isMailgunWebhookPayload(data: any): data is MailgunWebhookPayload {
return (
data &&
typeof data === 'object' &&
typeof data.signature === 'object' &&
typeof data['event-data'] === 'object'
);
}

16646
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,8 @@
"workspaces": [
"web-app",
"email-worker",
"shared-code"
"shared-code",
"mailgun-webhook"
],
"scripts": {
"install:all": "npm install",