feat: Ability to disable signups and disable email verification for self-hosters
This commit is contained in:
@@ -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)
|
||||||
# ========================================
|
# ========================================
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user