feat: Ability to disable signups and disable email verification for self-hosters

This commit is contained in:
Dries Augustyns
2026-01-12 15:20:14 +01:00
parent 4db1ccc3fc
commit 93165644af
6 changed files with 80 additions and 26 deletions
+15
View File
@@ -145,6 +145,21 @@ SMTP_DOMAIN=smtp.example.com
# Default: true # Default: true
# AUTO_PROJECT_DISABLE=false # AUTO_PROJECT_DISABLE=false
# ========================================
# OPTIONAL: Self-Hosting User Management
# ========================================
# Controls whether new user signups are allowed
# When enabled (true), the signup endpoint will reject new user registration attempts
# Useful for private instances or when you want to manually manage users
# Default: false
# DISABLE_SIGNUPS=false
# Controls whether email validation checks are performed on signup
# When enabled (true), validates emails for disposable domains, plus-addressing, domain existence, and MX records
# When disabled (false), skips these validation checks and allows any email format
# Default: false
# VERIFY_EMAIL_ON_SIGNUP=false
# ======================================== # ========================================
# ADVANCED (rarely needed) # ADVANCED (rarely needed)
# ======================================== # ========================================
+11 -4
View File
@@ -64,9 +64,9 @@ to run them separately (e.g., for debugging), use `dev:server` and `dev:worker`
### Applications (`apps/`) ### Applications (`apps/`)
- **api**: Express.js API server with TypeScript (ESM), uses @overnightjs/core - **api**: Express.js API server with TypeScript (ESM), uses @overnightjs/core
- HTTP API endpoints for the platform - HTTP API endpoints for the platform
- Background cron jobs (workflow processor, domain verification) - Background cron jobs (workflow processor, domain verification)
- **Worker process** (separate): BullMQ worker for processing email, campaign, and workflow queues - **Worker process** (separate): BullMQ worker for processing email, campaign, and workflow queues
- **web**: Next.js app (Pages Router) - Main platform (next-app.useplunk.com) - **web**: Next.js app (Pages Router) - Main platform (next-app.useplunk.com)
- **landing**: Next.js app (Pages Router) - Marketing site (next.useplunk.com) - **landing**: Next.js app (Pages Router) - Marketing site (next.useplunk.com)
- **wiki**: Next.js app - Documentation site (next-wiki.useplunk.com) - **wiki**: Next.js app - Documentation site (next-wiki.useplunk.com)
@@ -148,6 +148,12 @@ Required for builds and deployment (see turbo.json and .env.example):
- Notifications (optional): `NTFY_URL` (ntfy.sh topic URL or self-hosted server for system notifications) - Notifications (optional): `NTFY_URL` (ntfy.sh topic URL or self-hosted server for system notifications)
- Platform Email Notifications (optional): `PLUNK_API_KEY` (enables email notifications to users for critical events like - Platform Email Notifications (optional): `PLUNK_API_KEY` (enables email notifications to users for critical events like
project disabled, billing limits, etc. If not set, only ntfy notifications are sent) project disabled, billing limits, etc. If not set, only ntfy notifications are sent)
- Self-hosting User Management (optional):
- `DISABLE_SIGNUPS` (default: false) - When set to true, prevents new user signups via the API
- `VERIFY_EMAIL_ON_SIGNUP` (default: false) - When set to true, validates emails on signup for disposable domains,
plus-addressing, domain existence, and MX records
- Security (optional): `AUTO_PROJECT_DISABLE` (default: true) - Controls whether projects are automatically disabled when
bounce/complaint rate thresholds are exceeded
**Important Notes:** **Important Notes:**
@@ -159,7 +165,8 @@ Required for builds and deployment (see turbo.json and .env.example):
client-side access client-side access
## Plugins ## Plugins
There are two plugins installed for you to use. There are two plugins installed for you to use.
- frontend-design: This plugin can help you to create polished user interfaces. Use it when working on design-related tasks. - frontend-design: This plugin can help you to create polished user interfaces. Use it when working on design-related tasks.
- superpowers: This plugin can help you with advanced tasks such as refactorings, new features or architectural changes. Use it when you need extra assistance beyond basic coding. - superpowers: This plugin can help you with advanced tasks such as refactorings, new features or architectural changes. Use it when you need extra assistance beyond basic coding.
+6
View File
@@ -104,6 +104,12 @@ export const PLUNK_ENABLED = PLUNK_API_KEY !== '' && PLUNK_FROM_ADDRESS !== '';
// Useful for self-hosters who want to manage project status manually // Useful for self-hosters who want to manage project status manually
export const AUTO_PROJECT_DISABLE = validateEnv('AUTO_PROJECT_DISABLE', 'true') === 'true'; export const AUTO_PROJECT_DISABLE = validateEnv('AUTO_PROJECT_DISABLE', 'true') === 'true';
// Self-hosting Configuration (optional)
// Controls whether new user signups are allowed (default: false)
export const DISABLE_SIGNUPS = process.env.DISABLE_SIGNUPS === 'true';
// Controls whether email validation checks are performed on signup (default: false)
export const VERIFY_EMAIL_ON_SIGNUP = process.env.VERIFY_EMAIL_ON_SIGNUP === 'true';
// Email Verification & Password Reset // Email Verification & Password Reset
export const TOKEN_EXPIRY_SECONDS = 3600; // 1 hour export const TOKEN_EXPIRY_SECONDS = 3600; // 1 hour
export const EMAIL_VERIFICATION_RATE_LIMIT = 3; // Max 3 emails per hour export const EMAIL_VERIFICATION_RATE_LIMIT = 3; // Max 3 emails per hour
+34 -22
View File
@@ -7,6 +7,7 @@ import * as React from 'react';
import { import {
DASHBOARD_URI, DASHBOARD_URI,
DISABLE_SIGNUPS,
EMAIL_VERIFICATION_RATE_LIMIT, EMAIL_VERIFICATION_RATE_LIMIT,
EMAIL_VERIFICATION_RATE_WINDOW, EMAIL_VERIFICATION_RATE_WINDOW,
GITHUB_OAUTH_ENABLED, GITHUB_OAUTH_ENABLED,
@@ -15,6 +16,7 @@ import {
PASSWORD_RESET_RATE_LIMIT, PASSWORD_RESET_RATE_LIMIT,
PLUNK_ENABLED, PLUNK_ENABLED,
TOKEN_EXPIRY_SECONDS, TOKEN_EXPIRY_SECONDS,
VERIFY_EMAIL_ON_SIGNUP,
} from '../app/constants.js'; } from '../app/constants.js';
import {prisma} from '../database/prisma.js'; import {prisma} from '../database/prisma.js';
import {redis, REDIS_ONE_MINUTE} from '../database/redis.js'; import {redis, REDIS_ONE_MINUTE} from '../database/redis.js';
@@ -63,33 +65,43 @@ export class Auth {
@Post('signup') @Post('signup')
@CatchAsync @CatchAsync
public async signup(req: Request, res: Response, _next: NextFunction) { public async signup(req: Request, res: Response, _next: NextFunction) {
const {email, password} = AuthenticationSchemas.login.parse(req.body); // Check if signups are disabled
if (DISABLE_SIGNUPS) {
// Verify email is valid and not disposable/plus-addressed
const verification = await EmailVerificationService.verifyEmail(email);
if (
verification.isDisposable ||
verification.isPlusAddressed ||
!verification.domainExists ||
!verification.hasMxRecords
) {
// Build list of reasons for notification
const reasons: string[] = [];
if (verification.isDisposable) reasons.push('disposable email');
if (verification.isPlusAddressed) reasons.push('plus addressing');
if (!verification.domainExists) reasons.push('domain does not exist');
if (!verification.hasMxRecords) reasons.push('no MX records');
// Send notification about failed signup attempt
await NtfyService.notifyFailedSignupAttempt(email, reasons);
return res.json({ return res.json({
success: false, success: false,
data: 'This email address cannot be used for signup', data: 'New user signups are currently disabled',
}); });
} }
const {email, password} = AuthenticationSchemas.login.parse(req.body);
// Verify email is valid and not disposable/plus-addressed (if verification enabled)
if (VERIFY_EMAIL_ON_SIGNUP) {
const verification = await EmailVerificationService.verifyEmail(email);
if (
verification.isDisposable ||
verification.isPlusAddressed ||
!verification.domainExists ||
!verification.hasMxRecords
) {
// Build list of reasons for notification
const reasons: string[] = [];
if (verification.isDisposable) reasons.push('disposable email');
if (verification.isPlusAddressed) reasons.push('plus addressing');
if (!verification.domainExists) reasons.push('domain does not exist');
if (!verification.hasMxRecords) reasons.push('no MX records');
// Send notification about failed signup attempt
await NtfyService.notifyFailedSignupAttempt(email, reasons);
return res.json({
success: false,
data: 'This email address cannot be used for signup',
});
}
}
const user = await UserService.email(email); const user = await UserService.email(email);
if (user) { if (user) {
+7
View File
@@ -4,11 +4,13 @@ import type {NextFunction, Request, Response} from 'express';
import { import {
API_URI, API_URI,
DASHBOARD_URI, DASHBOARD_URI,
DISABLE_SIGNUPS,
GITHUB_OAUTH_CLIENT, GITHUB_OAUTH_CLIENT,
GITHUB_OAUTH_ENABLED, GITHUB_OAUTH_ENABLED,
GITHUB_OAUTH_SECRET, GITHUB_OAUTH_SECRET,
} from '../../app/constants.js'; } from '../../app/constants.js';
import {prisma} from '../../database/prisma.js'; import {prisma} from '../../database/prisma.js';
import {BadRequest} from '../../exceptions/index.js';
import {jwt} from '../../middleware/auth.js'; import {jwt} from '../../middleware/auth.js';
import {NtfyService} from '../../services/NtfyService.js'; import {NtfyService} from '../../services/NtfyService.js';
import {UserService} from '../../services/UserService.js'; import {UserService} from '../../services/UserService.js';
@@ -81,6 +83,11 @@ export class Github {
let isNewUser = false; let isNewUser = false;
if (!user) { if (!user) {
// Check if signups are disabled
if (DISABLE_SIGNUPS) {
throw new BadRequest('New user signups are currently disabled');
}
user = await prisma.user.create({ user = await prisma.user.create({
data: { data: {
email, email,
+7
View File
@@ -4,11 +4,13 @@ import type {NextFunction, Request, Response} from 'express';
import { import {
API_URI, API_URI,
DASHBOARD_URI, DASHBOARD_URI,
DISABLE_SIGNUPS,
GOOGLE_OAUTH_CLIENT, GOOGLE_OAUTH_CLIENT,
GOOGLE_OAUTH_ENABLED, GOOGLE_OAUTH_ENABLED,
GOOGLE_OAUTH_SECRET, GOOGLE_OAUTH_SECRET,
} from '../../app/constants.js'; } from '../../app/constants.js';
import {prisma} from '../../database/prisma.js'; import {prisma} from '../../database/prisma.js';
import {BadRequest} from '../../exceptions/index.js';
import {jwt} from '../../middleware/auth.js'; import {jwt} from '../../middleware/auth.js';
import {NtfyService} from '../../services/NtfyService.js'; import {NtfyService} from '../../services/NtfyService.js';
import {UserService} from '../../services/UserService.js'; import {UserService} from '../../services/UserService.js';
@@ -71,6 +73,11 @@ export class Google {
let isNewUser = false; let isNewUser = false;
if (!user) { if (!user) {
// Check if signups are disabled
if (DISABLE_SIGNUPS) {
throw new BadRequest('New user signups are currently disabled');
}
user = await prisma.user.create({ user = await prisma.user.create({
data: { data: {
email, email,