Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,20 @@ AMQP_EXCHANGE=safe-transaction-service-events
AMQP_QUEUE=safe-events-service
ADMIN_EMAIL=admin@safe
ADMIN_PASSWORD=password
ADMIN_COOKIE_SECRET=change-me-in-production
ADMIN_SESSION_SECRET=change-me-in-production
WEBHOOKS_CACHE_TTL=300000
NODE_ENV=dev
SSE_AUTH_TOKEN=aW5mcmFAc2FmZS5nbG9iYWw6YWJjMTIz
DATABASE_SSL_ENABLED=false
ADMIN_WEBHOOK_AUTH=super-secret-token
# DATABASE_CA_PATH=/path/of/db/certificate
# URL_BASE_PATH=/test # Set a globlal url path
# URL_BASE_PATH=/test # Set a global url path
# HTTP_TIMEOUT=1000 # Webhook HTTP client timeout in milliseconds (default: 1000)
# HTTP_MAX_REDIRECTS=0 # Max redirects for webhook HTTP client (default: 0)
# DB_HEALTH_CHECK_TIMEOUT=5000 # Database health check timeout in milliseconds (default: 5000)
# AMQP_PREFETCH_MESSAGES=10 # RabbitMQ prefetch message count (default: 10)
# WEBHOOK_AUTO_DISABLE=false # Auto-disable webhooks exceeding the failure threshold (default: false)
# WEBHOOK_FAILURE_THRESHOLD=90 # Failure rate percentage to trigger auto-disable (default: 90)
# WEBHOOK_HEALTH_MINUTES_WINDOW=60 # Rolling window in minutes for webhook health stats (default: 60)
# LOG_LEVEL=log # Logging level: verbose, debug, log, warn, error, fatal (default: log)
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ If you want to integrate with the events service, you need to:
- Endpoint need to answer with:
- `HTTP 202` status
- Nothing in the body.
- It should answer **as soon as possible**, as events service will timeout in 2 seconds, if multiple timeouts are detected **service will stop sending requests** to your endpoint. So you should receive the event, return a HTTP response and then act upon it.
- It should answer **as soon as possible**, as events service will timeout in 1 second by default (configurable via `HTTP_TIMEOUT`), if multiple timeouts are detected **service will stop sending requests** to your endpoint. So you should receive the event, return a HTTP response and then act upon it.
- Configuring HTTP Basic Auth in your endpoint is recommended so a malicious user cannot post fake events to your service.

## Events supported
Expand Down Expand Up @@ -268,6 +268,35 @@ npm run test:e2e
npm run test:cov
```

## Configuration

All configuration is done through environment variables. See `.env.sample` for a full template.

| Variable | Required | Default | Description |
|---|---|---|---|
| `DATABASE_URL` | Yes | — | PostgreSQL connection URL |
| `AMQP_URL` | Yes | — | RabbitMQ connection URL |
| `AMQP_EXCHANGE` | Yes | — | RabbitMQ exchange name |
| `AMQP_QUEUE` | Yes | `safe-events-service` | RabbitMQ queue name |
| `ADMIN_EMAIL` | Yes | — | Admin panel login email |
| `ADMIN_PASSWORD` | Yes | — | Admin panel login password |
| `ADMIN_COOKIE_SECRET` | Yes | — | Secret used to sign admin session cookies |
| `ADMIN_SESSION_SECRET` | Yes | — | Secret used to encrypt admin sessions |
| `ADMIN_WEBHOOK_AUTH` | Yes | — | Bearer token for webhook management endpoints |
| `SSE_AUTH_TOKEN` | No | `""` (disabled) | Base64 token for SSE endpoint (`Authorization: Basic <token>`). Auth is disabled when empty. |
| `NODE_ENV` | No | — | Set to `production` to disable schema auto-sync and enable production mode |
| `URL_BASE_PATH` | No | `""` | Global URL prefix (e.g. `/v1`) |
| `DATABASE_SSL_ENABLED` | No | `false` | Enable SSL for database connection |
| `DATABASE_CA_PATH` | No | — | Path to CA certificate file for database SSL |
| `HTTP_TIMEOUT` | No | `1000` | Webhook HTTP client timeout in milliseconds |
| `HTTP_MAX_REDIRECTS` | No | `0` | Max redirects followed when dispatching webhooks |
| `DB_HEALTH_CHECK_TIMEOUT` | No | `5000` | Database health check timeout in milliseconds |
| `AMQP_PREFETCH_MESSAGES` | No | `10` | RabbitMQ prefetch message count |
| `WEBHOOK_AUTO_DISABLE` | No | `false` | Auto-disable webhooks that exceed the failure threshold |
| `WEBHOOK_FAILURE_THRESHOLD` | No | `90` | Failure rate percentage (0–100) above which a webhook is auto-disabled |
| `WEBHOOK_HEALTH_MINUTES_WINDOW` | No | `60` | Rolling window in minutes used to compute per-webhook failure rates |
| `LOG_LEVEL` | No | `log` | Log verbosity: `verbose`, `debug`, `log`, `warn`, `error`, `fatal` |

## Creating database migrations

By default, the local dockerized migrations database will be used (test should not be used as it doesn't use migrations).
Expand Down
4 changes: 3 additions & 1 deletion scripts/docker_run.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#!/bin/bash
#!/bin/bash

set -e

export MIGRATIONS_DATABASE_URL=$DATABASE_URL
bash ./scripts/db_run_migrations_production.sh
Expand Down
4 changes: 2 additions & 2 deletions src/modules/admin/adminjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ async function buildAdminJsModule() {
authenticate: (email: string, password: string) =>
authService.authenticate(email, password),
cookieName: 'adminjs',
cookiePassword: 'secret',
cookiePassword: process.env.ADMIN_COOKIE_SECRET,
},
sessionOptions: {
resave: true,
saveUninitialized: true,
secret: 'secret',
secret: process.env.ADMIN_SESSION_SECRET,
},
}),
});
Expand Down
17 changes: 13 additions & 4 deletions src/modules/admin/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { timingSafeEqual } from 'crypto';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

Expand All @@ -20,10 +21,18 @@ export class AuthService {
const email = this.getAdminEmail();
const password = this.getAdminPassword();
const adminCredentials = { email, password };
if (
providedEmail === adminCredentials.email &&
providedPassword === adminCredentials.password
) {
const emailBuf = Buffer.from(providedEmail);
const storedEmailBuf = Buffer.from(adminCredentials.email);
const emailMatch =
emailBuf.byteLength === storedEmailBuf.byteLength &&
timingSafeEqual(emailBuf, storedEmailBuf);

const passwordBuf = Buffer.from(providedPassword);
const storedPasswordBuf = Buffer.from(adminCredentials.password);
const passwordMatch =
passwordBuf.byteLength === storedPasswordBuf.byteLength &&
timingSafeEqual(passwordBuf, storedPasswordBuf);
if (emailMatch && passwordMatch) {
return adminCredentials;
}
return undefined;
Expand Down
Loading