diff --git a/.env.example b/.env.example index 7f84578..dd216a8 100755 --- a/.env.example +++ b/.env.example @@ -70,4 +70,7 @@ SMTP_FROM_NAME=Docker Fullstack Template SMTP_ENCRYPTION=tls # Email verification setting -EMAIL_VERIFICATION_ENABLE=false \ No newline at end of file +EMAIL_VERIFICATION_ENABLE=false + +# Registration setting +REGISTRATION_ENABLE=true \ No newline at end of file diff --git a/backend/api/auth/controller.py b/backend/api/auth/controller.py index e1f8ca3..5a255d4 100644 --- a/backend/api/auth/controller.py +++ b/backend/api/auth/controller.py @@ -45,6 +45,7 @@ NotFoundException, ValidationException, EmailVerificationRequiredException, + RegistrationDisabledException, ) logger = logging.getLogger(__name__) @@ -58,7 +59,8 @@ responses=parse_responses({ 200: ("User registered successfully", UserLoginResponse), 202: ("Email verification required", None), - 409: ("Email already exists", None) + 409: ("Email already exists", None), + 503: ("Registration is disabled", None) }, common_responses) ) async def register_api( @@ -107,6 +109,8 @@ async def register_api( raise HTTPException(status_code=202, detail=resp.dict(exclude_none=True)) except ConflictException: raise HTTPException(status_code=409, detail="Email already exists") + except RegistrationDisabledException: + raise HTTPException(status_code=503, detail="Registration is disabled") except Exception: raise HTTPException(status_code=500) diff --git a/backend/api/auth/services.py b/backend/api/auth/services.py index 39b1c7a..c83c583 100644 --- a/backend/api/auth/services.py +++ b/backend/api/auth/services.py @@ -43,6 +43,7 @@ ServerException, ValidationException, EmailVerificationRequiredException, + RegistrationDisabledException, ) @@ -55,6 +56,9 @@ async def register( mailer: Optional[SMTPMailer] = None ) -> LoginResult: """User register""" + if not settings.REGISTRATION_ENABLE: + raise RegistrationDisabledException("Registration is disabled") + user = await _create_user(db, user_data) # Check if email verification is required diff --git a/backend/core/config.py b/backend/core/config.py index 01039d0..40cdf16 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -60,7 +60,10 @@ class Settings(BaseSettings): PASSWORD_MIN_LENGTH: int = 6 RATE_LIMIT: int = 200 RATE_LIMIT_WINDOW_SECONDS: int = 300 # 5 minutes - BLOCK_TIME_SECONDS: int = 600 # 10 minutes + BLOCK_TIME_SECONDS: int = 600 # 10 minutes + + # Registration settings + REGISTRATION_ENABLE: bool = True # SMTP setting SMTP_ENABLE: bool = False diff --git a/backend/tests/api/auth/test_controller.py b/backend/tests/api/auth/test_controller.py index 1df58b0..a6fceb8 100644 --- a/backend/tests/api/auth/test_controller.py +++ b/backend/tests/api/auth/test_controller.py @@ -11,7 +11,8 @@ NotFoundException, SMTPNotConfiguredException, ValidationException, - EmailVerificationRequiredException + EmailVerificationRequiredException, + RegistrationDisabledException, ) from core.security import ( verify_password_reset_token, @@ -105,6 +106,27 @@ async def test_register_email_verification_required(self, client: AsyncClient): assert data["code"] == 202 assert data["message"] == "Email verification required" + @pytest.mark.asyncio + async def test_register_disabled(self, client: AsyncClient): + """Test registration when registration is disabled""" + register_data = { + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "phone": "+1234567890", + "password": "TestPassword123!", + } + + with patch("api.auth.controller.register") as mock_register: + mock_register.side_effect = RegistrationDisabledException("Registration is disabled") + + response = await client.post("/api/auth/register", json=register_data) + + assert response.status_code == 503 + data = response.json() + assert data["code"] == 503 + assert data["message"] == "Registration is disabled" + @pytest.mark.asyncio async def test_register_server_error(self, client: AsyncClient): """Test registration with server error""" diff --git a/backend/tests/api/auth/test_service.py b/backend/tests/api/auth/test_service.py index 9b5b478..3845c3c 100644 --- a/backend/tests/api/auth/test_service.py +++ b/backend/tests/api/auth/test_service.py @@ -38,6 +38,7 @@ SMTPNotConfiguredException, ValidationException, EmailVerificationRequiredException, + RegistrationDisabledException, ) from core.config import settings @@ -57,17 +58,18 @@ async def test_register_success(self, test_db_session: AsyncSession): password="TestPassword123!", ) - result = await register( - test_db_session, mock_redis, user_data, "127.0.0.1", "TestAgent/1.0" - ) + with patch.object(settings, "REGISTRATION_ENABLE", True): + result = await register( + test_db_session, mock_redis, user_data, "127.0.0.1", "TestAgent/1.0" + ) - assert "user" in result - assert "session_id" in result - assert "access_token" in result - assert result["user"].email == user_data.email - assert result["user"].first_name == user_data.first_name - assert result["user"].last_name == user_data.last_name - assert result["user"].phone == user_data.phone + assert "user" in result + assert "session_id" in result + assert "access_token" in result + assert result["user"].email == user_data.email + assert result["user"].first_name == user_data.first_name + assert result["user"].last_name == user_data.last_name + assert result["user"].phone == user_data.phone @pytest.mark.asyncio async def test_register_email_already_exists( @@ -83,12 +85,33 @@ async def test_register_email_already_exists( password="TestPassword123!", ) - with pytest.raises(ConflictException) as exc_info: - await register( - test_db_session, mock_redis, user_data, "127.0.0.1", "TestAgent/1.0" - ) + with patch.object(settings, "REGISTRATION_ENABLE", True): + with pytest.raises(ConflictException) as exc_info: + await register( + test_db_session, mock_redis, user_data, "127.0.0.1", "TestAgent/1.0" + ) - assert "Email already exists" in str(exc_info.value) + assert "Email already exists" in str(exc_info.value) + + @pytest.mark.asyncio + async def test_register_disabled(self, test_db_session: AsyncSession): + """Test registration when registration is disabled""" + mock_redis = AsyncMock() + user_data = UserRegister( + first_name="John", + last_name="Doe", + email="john.doe@example.com", + phone="+1234567890", + password="TestPassword123!", + ) + + with patch.object(settings, "REGISTRATION_ENABLE", False): + with pytest.raises(RegistrationDisabledException) as exc_info: + await register( + test_db_session, mock_redis, user_data, "127.0.0.1", "TestAgent/1.0" + ) + + assert "Registration is disabled" in str(exc_info.value) @pytest.mark.asyncio async def test_login_success(self, test_db_session: AsyncSession, test_user: Users): @@ -696,9 +719,9 @@ async def test_register_email_verification_cooldown(self, test_db_session: Async password="TestPassword123!", ) - with patch.object(settings, "EMAIL_VERIFICATION_ENABLE", True), patch.object( - settings, "SMTP_ENABLE", True - ): + with patch.object(settings, "REGISTRATION_ENABLE", True), patch.object( + settings, "EMAIL_VERIFICATION_ENABLE", True + ), patch.object(settings, "SMTP_ENABLE", True): with pytest.raises(EmailVerificationRequiredException): await register( test_db_session, @@ -727,9 +750,9 @@ async def test_register_email_verification_send(self, test_db_session: AsyncSess password="TestPassword123!", ) - with patch.object(settings, "EMAIL_VERIFICATION_ENABLE", True), patch.object( - settings, "SMTP_ENABLE", True - ): + with patch.object(settings, "REGISTRATION_ENABLE", True), patch.object( + settings, "EMAIL_VERIFICATION_ENABLE", True + ), patch.object(settings, "SMTP_ENABLE", True): with pytest.raises(EmailVerificationRequiredException): await register( test_db_session, diff --git a/backend/utils/custom_exception.py b/backend/utils/custom_exception.py index 007b89d..282a868 100644 --- a/backend/utils/custom_exception.py +++ b/backend/utils/custom_exception.py @@ -78,4 +78,9 @@ def __init__(self, message: str = "Token error", details: Dict[str, Any] = None) class SMTPNotConfiguredException(BaseServiceException): """SMTP configuration related exceptions""" def __init__(self, message: str = "SMTP is not configured", details: Dict[str, Any] = None): - super().__init__(message=message, error_code="SMTP_NOT_CONFIGURED", details=details, status_code=503, log_level="warning") \ No newline at end of file + super().__init__(message=message, error_code="SMTP_NOT_CONFIGURED", details=details, status_code=503, log_level="warning") + +class RegistrationDisabledException(BaseServiceException): + """Registration disabled exception""" + def __init__(self, message: str = "Registration is disabled", details: Dict[str, Any] = None): + super().__init__(message=message, error_code="REGISTRATION_DISABLED", details=details, status_code=503, log_level="warning") \ No newline at end of file diff --git a/docker-compose-prod.yaml b/docker-compose-prod.yaml index 79e0dc5..cbea83c 100755 --- a/docker-compose-prod.yaml +++ b/docker-compose-prod.yaml @@ -69,6 +69,8 @@ services: - VITE_API_PORT=${API_PORT} # SMTP settings - VITE_SMTP_ENABLE=${SMTP_ENABLE} + # Registration settings + - VITE_REGISTRATION_ENABLE=${REGISTRATION_ENABLE} command: ["sh", "./init-prod.sh"] depends_on: backend: @@ -123,6 +125,8 @@ services: - SMTP_ENCRYPTION=${SMTP_ENCRYPTION} # Email verification settings - EMAIL_VERIFICATION_ENABLE=${EMAIL_VERIFICATION_ENABLE} + # Registration settings + - REGISTRATION_ENABLE=${REGISTRATION_ENABLE} command: ["sh", "./init-prod.sh"] depends_on: mariadb: diff --git a/docker-compose.yaml b/docker-compose.yaml index 8de1f62..9a3ba96 100755 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -68,6 +68,8 @@ services: - VITE_API_PORT=${API_PORT:-5000} # SMTP settings - VITE_SMTP_ENABLE=${SMTP_ENABLE:-false} + # Registration settings + - VITE_REGISTRATION_ENABLE=${REGISTRATION_ENABLE:-true} command: ["sh", "./init-dev.sh"] depends_on: backend: @@ -122,6 +124,8 @@ services: - SMTP_ENCRYPTION=${SMTP_ENCRYPTION:-tls} # Email verification settings - EMAIL_VERIFICATION_ENABLE=${EMAIL_VERIFICATION_ENABLE:-false} + # Registration settings + - REGISTRATION_ENABLE=${REGISTRATION_ENABLE:-true} command: ["sh", "./init-dev.sh"] depends_on: mariadb: diff --git a/frontend/src/components/auth/login-form.jsx b/frontend/src/components/auth/login-form.jsx index 4cb7c7a..c0f7b81 100644 --- a/frontend/src/components/auth/login-form.jsx +++ b/frontend/src/components/auth/login-form.jsx @@ -370,16 +370,18 @@ export const LoginForm = ({ className, redirectTo = '/', ...props }) => { isSubmitting={isSubmitting} /> -
- {t('pages.auth.login.links.newUser', { defaultValue: 'New user? ' })} - - {t('pages.auth.login.links.register', { defaultValue: 'Sign up' })} - -
+ {ENV.REGISTRATION_ENABLE && ( +
+ {t('pages.auth.login.links.newUser', { defaultValue: 'New user? ' })} + + {t('pages.auth.login.links.register', { defaultValue: 'Sign up' })} + +
+ )} ) diff --git a/frontend/src/config/env.config.js b/frontend/src/config/env.config.js index ecf1ca1..815900b 100755 --- a/frontend/src/config/env.config.js +++ b/frontend/src/config/env.config.js @@ -13,6 +13,8 @@ export const ENV = { API_PORT: import.meta.env.VITE_API_PORT || 5000, // SMTP settings SMTP_ENABLE: import.meta.env.VITE_SMTP_ENABLE === 'true' || false, + // Registration settings + REGISTRATION_ENABLE: import.meta.env.VITE_REGISTRATION_ENABLE === 'true' || false, }; export default ENV; \ No newline at end of file diff --git a/frontend/src/router/routes.js b/frontend/src/router/routes.js index e5f018c..e59e608 100755 --- a/frontend/src/router/routes.js +++ b/frontend/src/router/routes.js @@ -64,7 +64,8 @@ export const routes = [ showInSidebar: false, }, }, - { + // Only include register route when registration is enabled + ...(ENV.REGISTRATION_ENABLE ? [{ path: "/auth/register", element: "Auth", requireAuth: false, @@ -72,7 +73,7 @@ export const routes = [ sidebar: { showInSidebar: false, }, - }, + }] : []), { path: "/auth/reset-password", element: "Auth",