From 863e784e1c806118204acb3ba489322fe51498ad Mon Sep 17 00:00:00 2001 From: Dries Augustyns Date: Tue, 16 Dec 2025 11:52:53 +0100 Subject: [PATCH] fix: Add additional checks for disabled projects --- .env.self-host.example | 10 +++++++++ apps/api/.env.example | 8 +++++++ apps/api/src/app/constants.ts | 5 +++++ apps/api/src/services/SecurityService.ts | 28 +++++++++++++++++++++--- docker-compose.yml | 3 +++ 5 files changed, 51 insertions(+), 3 deletions(-) diff --git a/.env.self-host.example b/.env.self-host.example index 1066999..8d97f19 100644 --- a/.env.self-host.example +++ b/.env.self-host.example @@ -121,6 +121,16 @@ SMTP_DOMAIN=smtp.example.com # PLUNK_API_KEY= # PLUNK_FROM_ADDRESS= +# ======================================== +# OPTIONAL: Security Settings +# ======================================== +# Controls whether projects are automatically disabled when bounce/complaint rate thresholds are exceeded +# When enabled (default), projects exceeding security limits will be automatically suspended +# When disabled, violations will be logged and notifications sent, but projects won't be auto-disabled +# Recommended for self-hosters: false (manage project status manually) +# Default: true +# AUTO_PROJECT_DISABLE=false + # ======================================== # ADVANCED (rarely needed) # ======================================== diff --git a/apps/api/.env.example b/apps/api/.env.example index 324b73c..e0063f3 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -76,3 +76,11 @@ STRIPE_WEBHOOK_SECRET= STRIPE_PRICE_ONBOARDING= # Optional: One-time onboarding fee price ID (e.g., price_xxxxx) STRIPE_PRICE_EMAIL_USAGE= # Required: Metered price ID for pay-per-email billing STRIPE_METER_EVENT_NAME=emails # Meter event name (API key from your Stripe meter, default: emails) + +# ============================================================================== +# Security (Optional) +# ============================================================================== +# Controls whether projects are automatically disabled when bounce/complaint rate thresholds are exceeded +# Set to 'false' to disable automatic project suspension (useful for self-hosters who manage manually) +# Default: true (automatic project disabling enabled) +# AUTO_PROJECT_DISABLE=true diff --git a/apps/api/src/app/constants.ts b/apps/api/src/app/constants.ts index a640b25..73660e9 100644 --- a/apps/api/src/app/constants.ts +++ b/apps/api/src/app/constants.ts @@ -98,3 +98,8 @@ export const SMTP_ENABLED = export const PLUNK_API_KEY = validateEnv('PLUNK_API_KEY', ''); export const PLUNK_FROM_ADDRESS = validateEnv('PLUNK_FROM_ADDRESS', ''); export const PLUNK_ENABLED = PLUNK_API_KEY !== '' && PLUNK_FROM_ADDRESS !== ''; + +// Security (optional) +// Controls whether projects are automatically disabled when bounce/complaint rate thresholds are exceeded +// Useful for self-hosters who want to manage project status manually +export const AUTO_PROJECT_DISABLE = validateEnv('AUTO_PROJECT_DISABLE', 'true') === 'true'; diff --git a/apps/api/src/services/SecurityService.ts b/apps/api/src/services/SecurityService.ts index d374182..c852e84 100644 --- a/apps/api/src/services/SecurityService.ts +++ b/apps/api/src/services/SecurityService.ts @@ -7,7 +7,7 @@ import {redis} from '../database/redis.js'; import {Keys} from './keys.js'; import {NtfyService} from './NtfyService.js'; import {QueueService} from './QueueService.js'; -import {DASHBOARD_URI, LANDING_URI} from '../app/constants.js'; +import {AUTO_PROJECT_DISABLE, DASHBOARD_URI, LANDING_URI} from '../app/constants.js'; /** * Security thresholds for bounce and complaint rates @@ -110,9 +110,31 @@ export class SecurityService { // Get current security status const status = await this.getSecurityStatus(projectId); - // If project should be disabled, disable it - if (status.shouldDisable) { + // If project should be disabled, disable it (only if auto-disable is enabled) + if (status.shouldDisable && AUTO_PROJECT_DISABLE) { await this.disableProject(projectId, status); + } else if (status.shouldDisable && !AUTO_PROJECT_DISABLE) { + // Log critical violations but don't auto-disable (self-hosted mode) + const project = await prisma.project.findUnique({ + where: {id: projectId}, + select: {name: true}, + }); + + if (project) { + signale.error( + `[SECURITY] Project ${projectId} (${project.name}) has CRITICAL security violations but auto-disable is turned off:`, + status.violations, + ); + signale.info( + `[SECURITY] 7-day stats: ${status.sevenDay.bounces} bounces, ${status.sevenDay.complaints} complaints out of ${status.sevenDay.total} emails`, + ); + signale.info( + `[SECURITY] All-time stats: ${status.allTime.bounces} bounces, ${status.allTime.complaints} complaints out of ${status.allTime.total} emails`, + ); + + // Send notification about critical security violations + await NtfyService.notifySecurityWarning(project.name, projectId, status.violations); + } } else if (status.warnings.length > 0) { // Log warnings for monitoring signale.warn(`[SECURITY] Project ${projectId} has security warnings:`, status.warnings); diff --git a/docker-compose.yml b/docker-compose.yml index 6261e17..e1f136c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -187,6 +187,9 @@ services: PLUNK_API_KEY: ${PLUNK_API_KEY:-} PLUNK_FROM_ADDRESS: ${PLUNK_FROM_ADDRESS:-} + # Security + AUTO_PROJECT_DISABLE: ${AUTO_PROJECT_DISABLE:-false} + volumes: # Persistent storage for application data - plunk_data:/app/data