mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-22 23:31:12 +02:00
Add disable_registration toggle to site configuration
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 <noreply@anthropic.com>
This commit is contained in:
parent
e79fd35a94
commit
b8bb74ec3e
9 changed files with 120 additions and 26 deletions
|
|
@ -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')
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-4 text-center text-sm">
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
{t("dont_have_account")}{" "}
|
||||
<Link
|
||||
href="/register"
|
||||
className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400"
|
||||
>
|
||||
{t("sign_up")}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
{!config.disable_registration && (
|
||||
<div className="mt-4 text-center text-sm">
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
{t("dont_have_account")}{" "}
|
||||
<Link
|
||||
href="/register"
|
||||
className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400"
|
||||
>
|
||||
{t("sign_up")}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="relative w-full overflow-hidden">
|
||||
<AmbientBackground />
|
||||
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center">
|
||||
<Logo className="rounded-md" />
|
||||
<h1 className="my-8 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:text-4xl">
|
||||
{t("create_account")}
|
||||
</h1>
|
||||
<RouteGuard routeKey="registration">
|
||||
<div className="relative w-full overflow-hidden">
|
||||
<AmbientBackground />
|
||||
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center">
|
||||
<Logo className="rounded-md" />
|
||||
<h1 className="my-8 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:text-4xl">
|
||||
{t("create_account")}
|
||||
</h1>
|
||||
|
||||
<div className="w-full max-w-md">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
|
|
@ -296,5 +298,6 @@ export default function RegisterPage() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RouteGuard>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* Registration Control Section */}
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Registration Control
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
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.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<ToggleSwitch
|
||||
label="Disable Registration"
|
||||
description="Prevent new users from creating accounts"
|
||||
checked={formData.disable_registration}
|
||||
onChange={() => handleToggle("disable_registration")}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Custom Text Section */}
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue