From b8bb74ec3ed89175d5075006ad4e22383bd4d6cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oj=C4=81rs=20Kapteinis?= Date: Tue, 18 Nov 2025 14:28:16 +0200 Subject: [PATCH] Add disable_registration toggle to site configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented comprehensive registration control system that allows administrators to disable new user registrations through the admin panel. This provides better control over user access and supports closed registration scenarios. Backend changes: - Added disable_registration field to SiteConfiguration model (db.py) - Created migration #39 to add disable_registration column with default false - Updated SiteConfigurationBase, Update, and Read schemas - Enhanced registration_allowed() dependency to check database toggle - Returns 403 with clear message when registration is disabled Frontend changes: - Added disable_registration to SiteConfig interface and default config - Updated LocalLoginForm to conditionally hide "Sign up" link when disabled - Added RouteGuard support for "registration" route key - Protected /register page with RouteGuard (shows 404 when disabled) - Added "Registration Control" section to admin site-settings page 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- ...able_registration_to_site_configuration.py | 31 +++++++++++++++++++ surfsense_backend/app/app.py | 20 ++++++++++-- surfsense_backend/app/db.py | 3 ++ .../app/schemas/site_configuration.py | 4 +++ .../app/(home)/login/LocalLoginForm.tsx | 26 +++++++++------- surfsense_web/app/(home)/register/page.tsx | 17 +++++----- .../app/dashboard/site-settings/page.tsx | 25 +++++++++++++++ surfsense_web/components/RouteGuard.tsx | 16 +++++++--- surfsense_web/contexts/SiteConfigContext.tsx | 4 +++ 9 files changed, 120 insertions(+), 26 deletions(-) create mode 100644 surfsense_backend/alembic/versions/39_add_disable_registration_to_site_configuration.py diff --git a/surfsense_backend/alembic/versions/39_add_disable_registration_to_site_configuration.py b/surfsense_backend/alembic/versions/39_add_disable_registration_to_site_configuration.py new file mode 100644 index 000000000..5a9130ea2 --- /dev/null +++ b/surfsense_backend/alembic/versions/39_add_disable_registration_to_site_configuration.py @@ -0,0 +1,31 @@ +"""add disable_registration to site_configuration + +Revision ID: 39_add_disable_registration_to_site_configuration +Revises: 38_add_site_configuration_table +Create Date: 2025-11-18 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '39_add_disable_registration_to_site_configuration' +down_revision: Union[str, None] = '38_add_site_configuration_table' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add disable_registration column to site_configuration table + op.add_column( + 'site_configuration', + sa.Column('disable_registration', sa.Boolean(), nullable=False, server_default='false') + ) + + +def downgrade() -> None: + # Remove disable_registration column + op.drop_column('site_configuration', 'disable_registration') diff --git a/surfsense_backend/app/app.py b/surfsense_backend/app/app.py index 3fc0b14f7..7925a8945 100644 --- a/surfsense_backend/app/app.py +++ b/surfsense_backend/app/app.py @@ -2,11 +2,12 @@ from contextlib import asynccontextmanager from fastapi import Depends, FastAPI, HTTPException, status from fastapi.middleware.cors import CORSMiddleware +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware from app.config import config -from app.db import User, create_db_and_tables, get_async_session +from app.db import SiteConfiguration, User, create_db_and_tables, get_async_session from app.routes import router as crud_router from app.schemas import UserCreate, UserRead, UserUpdate from app.users import SECRET, auth_backend, current_active_user, fastapi_users @@ -19,11 +20,24 @@ async def lifespan(app: FastAPI): yield -def registration_allowed(): +async def registration_allowed(session: AsyncSession = Depends(get_async_session)): + # Check environment variable first if not config.REGISTRATION_ENABLED: raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail="Registration is disabled" + status_code=status.HTTP_403_FORBIDDEN, + detail="Registration is disabled by system configuration" ) + + # Check site configuration database toggle + result = await session.execute(select(SiteConfiguration).where(SiteConfiguration.id == 1)) + site_config = result.scalar_one_or_none() + + if site_config and site_config.disable_registration: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Registration is currently disabled. Please contact the administrator if you need access." + ) + return True diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py index 12d07e5fe..f67d06215 100644 --- a/surfsense_backend/app/db.py +++ b/surfsense_backend/app/db.py @@ -411,6 +411,9 @@ class SiteConfiguration(Base): disable_terms_route = Column(Boolean, nullable=False, default=True) disable_privacy_route = Column(Boolean, nullable=False, default=True) + # Registration control + disable_registration = Column(Boolean, nullable=False, default=False) + # Custom text custom_copyright = Column(String(200), nullable=True, default="SurfSense 2025") diff --git a/surfsense_backend/app/schemas/site_configuration.py b/surfsense_backend/app/schemas/site_configuration.py index ca790931b..48329335d 100644 --- a/surfsense_backend/app/schemas/site_configuration.py +++ b/surfsense_backend/app/schemas/site_configuration.py @@ -24,6 +24,9 @@ class SiteConfigurationBase(BaseModel): disable_terms_route: bool = True disable_privacy_route: bool = True + # Registration control + disable_registration: bool = False + # Custom text custom_copyright: str | None = Field(default="SurfSense 2025", max_length=200) @@ -44,6 +47,7 @@ class SiteConfigurationUpdate(SiteConfigurationBase): disable_contact_route: bool | None = None disable_terms_route: bool | None = None disable_privacy_route: bool | None = None + disable_registration: bool | None = None custom_copyright: str | None = None diff --git a/surfsense_web/app/(home)/login/LocalLoginForm.tsx b/surfsense_web/app/(home)/login/LocalLoginForm.tsx index 5bb4cbc40..b3d7d146c 100644 --- a/surfsense_web/app/(home)/login/LocalLoginForm.tsx +++ b/surfsense_web/app/(home)/login/LocalLoginForm.tsx @@ -8,12 +8,14 @@ import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { loginMutationAtom } from "@/atoms/auth/auth-mutation.atoms"; +import { useSiteConfig } from "@/contexts/SiteConfigContext"; import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors"; import { ValidationError } from "@/lib/error"; export function LocalLoginForm() { const t = useTranslations("auth"); const tCommon = useTranslations("common"); + const { config } = useSiteConfig(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [showPassword, setShowPassword] = useState(false); @@ -227,17 +229,19 @@ export function LocalLoginForm() { -
-

- {t("dont_have_account")}{" "} - - {t("sign_up")} - -

-
+ {!config.disable_registration && ( +
+

+ {t("dont_have_account")}{" "} + + {t("sign_up")} + +

+
+ )} ); } diff --git a/surfsense_web/app/(home)/register/page.tsx b/surfsense_web/app/(home)/register/page.tsx index 58a729630..4bb97dbac 100644 --- a/surfsense_web/app/(home)/register/page.tsx +++ b/surfsense_web/app/(home)/register/page.tsx @@ -9,6 +9,7 @@ import { useEffect, useState } from "react"; import { toast } from "sonner"; import { registerMutationAtom } from "@/atoms/auth/auth-mutation.atoms"; import { Logo } from "@/components/Logo"; +import { RouteGuard } from "@/components/RouteGuard"; import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors"; import { AppError, ValidationError } from "@/lib/error"; import { AmbientBackground } from "../login/AmbientBackground"; @@ -136,13 +137,14 @@ export default function RegisterPage() { }; return ( -
- -
- -

- {t("create_account")} -

+ +
+ +
+ +

+ {t("create_account")} +

@@ -296,5 +298,6 @@ export default function RegisterPage() {
+
); } diff --git a/surfsense_web/app/dashboard/site-settings/page.tsx b/surfsense_web/app/dashboard/site-settings/page.tsx index 4e157f3a3..787acf863 100644 --- a/surfsense_web/app/dashboard/site-settings/page.tsx +++ b/surfsense_web/app/dashboard/site-settings/page.tsx @@ -28,6 +28,9 @@ interface SiteConfigForm { disable_terms_route: boolean; disable_privacy_route: boolean; + // Registration control + disable_registration: boolean; + // Custom text custom_copyright: string; } @@ -49,6 +52,7 @@ export default function SiteSettingsPage() { disable_contact_route: true, disable_terms_route: true, disable_privacy_route: true, + disable_registration: false, custom_copyright: "SurfSense 2025", }); const [isSaving, setIsSaving] = useState(false); @@ -71,6 +75,7 @@ export default function SiteSettingsPage() { disable_contact_route: config.disable_contact_route, disable_terms_route: config.disable_terms_route, disable_privacy_route: config.disable_privacy_route, + disable_registration: config.disable_registration, custom_copyright: config.custom_copyright || "SurfSense 2025", }); } @@ -257,6 +262,26 @@ export default function SiteSettingsPage() {
+ {/* Registration Control Section */} +
+

+ Registration Control +

+

+ Control user registration availability. When disabled, the Sign Up link will be hidden + and the registration page will show a 404 error. The backend will also block registration + requests with a 403 error. +

+
+ handleToggle("disable_registration")} + /> +
+
+ {/* Custom Text Section */}

diff --git a/surfsense_web/components/RouteGuard.tsx b/surfsense_web/components/RouteGuard.tsx index bbde9d1fc..f7f1ed83e 100644 --- a/surfsense_web/components/RouteGuard.tsx +++ b/surfsense_web/components/RouteGuard.tsx @@ -6,7 +6,7 @@ import { useSiteConfig } from "@/contexts/SiteConfigContext"; interface RouteGuardProps { children: React.ReactNode; - routeKey: "pricing" | "docs" | "contact" | "terms" | "privacy"; + routeKey: "pricing" | "docs" | "contact" | "terms" | "privacy" | "registration"; } export function RouteGuard({ children, routeKey }: RouteGuardProps) { @@ -16,8 +16,11 @@ export function RouteGuard({ children, routeKey }: RouteGuardProps) { useEffect(() => { if (isLoading) return; - const disableKey = `disable_${routeKey}_route` as keyof typeof config; - const isDisabled = config[disableKey]; + // Special case: registration uses disable_registration instead of disable_registration_route + const disableKey = routeKey === "registration" + ? "disable_registration" + : (`disable_${routeKey}_route` as keyof typeof config); + const isDisabled = config[disableKey as keyof typeof config]; if (isDisabled) { router.replace("/404"); @@ -34,8 +37,11 @@ export function RouteGuard({ children, routeKey }: RouteGuardProps) { } // Check if route is disabled - const disableKey = `disable_${routeKey}_route` as keyof typeof config; - const isDisabled = config[disableKey]; + // Special case: registration uses disable_registration instead of disable_registration_route + const disableKey = routeKey === "registration" + ? "disable_registration" + : (`disable_${routeKey}_route` as keyof typeof config); + const isDisabled = config[disableKey as keyof typeof config]; if (isDisabled) { return null; diff --git a/surfsense_web/contexts/SiteConfigContext.tsx b/surfsense_web/contexts/SiteConfigContext.tsx index 6364cd94b..7d4a63a74 100644 --- a/surfsense_web/contexts/SiteConfigContext.tsx +++ b/surfsense_web/contexts/SiteConfigContext.tsx @@ -25,6 +25,9 @@ export interface SiteConfig { disable_terms_route: boolean; disable_privacy_route: boolean; + // Registration control + disable_registration: boolean; + // Custom text custom_copyright: string | null; } @@ -44,6 +47,7 @@ const defaultConfig: SiteConfig = { disable_contact_route: true, disable_terms_route: true, disable_privacy_route: true, + disable_registration: false, custom_copyright: "SurfSense 2025", };