Compare commits
10 Commits
45d5507bf9
...
feature/em
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a3a02bd6d | ||
|
|
3d02654510 | ||
|
|
0faac8e392 | ||
|
|
f9f33a2b45 | ||
|
|
ccc690c369 | ||
| d2725261d5 | |||
| f169e2c4ba | |||
| c72a06e34e | |||
| 9d6507c3ae | |||
| 6cf9b312c0 |
36
ci-build.sh
Executable file
36
ci-build.sh
Executable 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"
|
||||
@@ -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`)
|
||||
|
||||
|
||||
@@ -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`)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]$ ]]
|
||||
|
||||
4
mailgun-webhook/package-lock.json
generated
4
mailgun-webhook/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
16646
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,8 @@
|
||||
"workspaces": [
|
||||
"web-app",
|
||||
"email-worker",
|
||||
"shared-code"
|
||||
"shared-code",
|
||||
"mailgun-webhook"
|
||||
],
|
||||
"scripts": {
|
||||
"install:all": "npm install",
|
||||
|
||||
Reference in New Issue
Block a user