diff --git a/README.md b/README.md index a032e1f..f8d85fd 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ The server starts on port 3001 (HTTP). If SSL certificates are present in `serve | `PORT` | HTTP port | `3001` | | `HTTPS_PORT` | HTTPS port (used when SSL certs are present) | `3443` | | `SELF_HOSTED` | First user gets all features unlocked | `false` | +| `DISABLE_REGISTRATION` | Block new account creation (including OAuth auto-signup). First-user setup on an empty DB is still allowed. | `false` | | `APP_URL` | Your public URL (used for Stripe callbacks) | _(none)_ | | `JWT_SECRET` | JWT signing key (auto-generated if not set) | _(auto)_ | | `SSL_CERT` | Path to SSL certificate | `server/certs/cert.pem` | diff --git a/frontend/js/views/login.js b/frontend/js/views/login.js index b8cc660..de67329 100644 --- a/frontend/js/views/login.js +++ b/frontend/js/views/login.js @@ -12,6 +12,8 @@ async function loadAuthConfig() { export async function render(container) { const config = await loadAuthConfig(); const isSetup = config.needsSetup; + // registration_enabled may be absent on older servers — treat as enabled for back-compat + const canRegister = config.registration_enabled !== false; container.innerHTML = `
@@ -26,7 +28,7 @@ export async function render(container) {

${isSetup ? 'Create your admin account to get started' : 'Sign in to manage your displays'}

- ${isSetup ? '' : '

New accounts get a 14-day free Pro trial

'} + ${!isSetup && canRegister ? '

New accounts get a 14-day free Pro trial

' : ''}
@@ -49,7 +51,7 @@ export async function render(container) { - ${!isSetup ? ` + ${!isSetup && canRegister ? ` diff --git a/server/config.js b/server/config.js index bb8e698..34a1c9e 100644 --- a/server/config.js +++ b/server/config.js @@ -39,4 +39,7 @@ module.exports = { emailWebhookUrl: process.env.EMAIL_WEBHOOK_URL || '', // Self-hosted mode: if true, first user gets enterprise plan and no billing selfHosted: process.env.SELF_HOSTED === 'true', + // Disable public registration (OAuth auto-signup is also blocked when set). + // First-user setup is still allowed so a fresh install can be initialized. + disableRegistration: ['true', '1'].includes(String(process.env.DISABLE_REGISTRATION || '').toLowerCase()), }; diff --git a/server/routes/auth.js b/server/routes/auth.js index cc54753..0731827 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -25,8 +25,19 @@ function logSuccessfulLogin(userId, email, ip) { // ==================== Local Auth ==================== +// Returns true if new account creation is allowed at this moment. +// First-user setup (empty DB) is always allowed so a fresh install can be initialized. +function canRegister() { + if (!config.disableRegistration) return true; + const userCount = db.prepare('SELECT COUNT(*) as count FROM users').get().count; + return userCount === 0; +} + // Register router.post('/register', (req, res) => { + if (!canRegister()) { + return res.status(403).json({ error: 'Public registration is disabled. Contact your administrator.' }); + } const { email, password, name } = req.body; if (!email || !password) return res.status(400).json({ error: 'Email and password required' }); if (password.length < 8) return res.status(400).json({ error: 'Password must be at least 8 characters' }); @@ -94,6 +105,9 @@ router.post('/google', async (req, res) => { let user = db.prepare('SELECT * FROM users WHERE email = ?').get(email.toLowerCase()); if (!user) { + if (!canRegister()) { + return res.status(403).json({ error: 'Public registration is disabled. Contact your administrator.' }); + } const id = uuidv4(); const userCount = db.prepare('SELECT COUNT(*) as count FROM users').get().count; const role = userCount === 0 ? 'superadmin' : 'user'; @@ -169,6 +183,9 @@ router.post('/microsoft', async (req, res) => { let user = db.prepare('SELECT * FROM users WHERE email = ?').get(email); if (!user) { + if (!canRegister()) { + return res.status(403).json({ error: 'Public registration is disabled. Contact your administrator.' }); + } const id = uuidv4(); const userCount = db.prepare('SELECT COUNT(*) as count FROM users').get().count; const role = userCount === 0 ? 'superadmin' : 'user'; @@ -297,6 +314,7 @@ router.get('/config', (req, res) => { microsoftTenantId: config.microsoftTenantId, localEnabled: true, needsSetup: userCount === 0, + registration_enabled: !config.disableRegistration || userCount === 0, }); });