Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,7 @@ SMTP_FROM_NAME=Docker Fullstack Template
SMTP_ENCRYPTION=tls

# Email verification setting
EMAIL_VERIFICATION_ENABLE=false
EMAIL_VERIFICATION_ENABLE=false

# Registration setting
REGISTRATION_ENABLE=true
6 changes: 5 additions & 1 deletion backend/api/auth/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
NotFoundException,
ValidationException,
EmailVerificationRequiredException,
RegistrationDisabledException,
)

logger = logging.getLogger(__name__)
Expand All @@ -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(
Expand Down Expand Up @@ -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)

Expand Down
4 changes: 4 additions & 0 deletions backend/api/auth/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
ServerException,
ValidationException,
EmailVerificationRequiredException,
RegistrationDisabledException,
)


Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion backend/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 23 additions & 1 deletion backend/tests/api/auth/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
NotFoundException,
SMTPNotConfiguredException,
ValidationException,
EmailVerificationRequiredException
EmailVerificationRequiredException,
RegistrationDisabledException,
)
from core.security import (
verify_password_reset_token,
Expand Down Expand Up @@ -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"""
Expand Down
65 changes: 44 additions & 21 deletions backend/tests/api/auth/test_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
SMTPNotConfiguredException,
ValidationException,
EmailVerificationRequiredException,
RegistrationDisabledException,
)
from core.config import settings

Expand All @@ -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(
Expand All @@ -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):
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion backend/utils/custom_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
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")
4 changes: 4 additions & 0 deletions docker-compose-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
22 changes: 12 additions & 10 deletions frontend/src/components/auth/login-form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -370,16 +370,18 @@ export const LoginForm = ({ className, redirectTo = '/', ...props }) => {
isSubmitting={isSubmitting}
/>

<div className="text-center text-sm flex items-center justify-center gap-2">
<span className="text-muted-foreground">{t('pages.auth.login.links.newUser', { defaultValue: 'New user? ' })}</span>
<Link
to="/auth/register"
className="font-medium text-primary hover:underline"
state={location.state}
>
{t('pages.auth.login.links.register', { defaultValue: 'Sign up' })}
</Link>
</div>
{ENV.REGISTRATION_ENABLE && (
<div className="text-center text-sm flex items-center justify-center gap-2">
<span className="text-muted-foreground">{t('pages.auth.login.links.newUser', { defaultValue: 'New user? ' })}</span>
<Link
to="/auth/register"
className="font-medium text-primary hover:underline"
state={location.state}
>
{t('pages.auth.login.links.register', { defaultValue: 'Sign up' })}
</Link>
</div>
)}
</form>
</Form>
)
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/config/env.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
5 changes: 3 additions & 2 deletions frontend/src/router/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,16 @@ export const routes = [
showInSidebar: false,
},
},
{
// Only include register route when registration is enabled
...(ENV.REGISTRATION_ENABLE ? [{
path: "/auth/register",
element: "Auth",
requireAuth: false,
permissions: [],
sidebar: {
showInSidebar: false,
},
},
}] : []),
{
path: "/auth/reset-password",
element: "Auth",
Expand Down