diff --git a/backend/api/auth/schema.py b/backend/api/auth/schema.py index 82dd677..9198cdf 100644 --- a/backend/api/auth/schema.py +++ b/backend/api/auth/schema.py @@ -6,17 +6,25 @@ from core.config import settings +class UserResponse(BaseModel): + id: str = Field(..., description="User ID") + first_name: str = Field(..., description="First name") + last_name: str = Field(..., description="Last name") + email: str = Field(..., description="User email address") + phone: str = Field(..., description="Phone number") + + class LoginResult(TypedDict): user: UserResponse - session_id: str = Field(..., description="Session ID") - access_token: str = Field(..., description="JWT access token") - csrf_token: str = Field(..., description="CSRF token") + session_id: str + access_token: str + csrf_token: str class SessionResult(TypedDict): - session_id: str = Field(..., description="Session ID") - access_token: str = Field(..., description="JWT access token") - csrf_token: str = Field(..., description="CSRF token") + session_id: str + access_token: str + csrf_token: str class UserRegister(BaseModel): @@ -34,14 +42,6 @@ class UserLogin(BaseModel): password: str = Field(..., min_length=1, description="Password") -class UserResponse(BaseModel): - id: str = Field(..., description="User ID") - first_name: str = Field(..., description="First name") - last_name: str = Field(..., description="Last name") - email: str = Field(..., description="User email address") - phone: str = Field(..., description="Phone number") - - class UserLoginResponse(BaseModel): access_token: str = Field(..., description="JWT access token") expires_at: datetime = Field(..., description="Token expiration time") @@ -89,19 +89,6 @@ class PasswordResetCooldownResponse(BaseModel): cooldown_seconds: int = Field(..., description="Remaining cooldown time in seconds") -class EmailVerificationResponse(BaseModel): - message: str = Field(..., description="Verification result message") - - -class EmailVerificationRequiredResponse(BaseModel): - expires_at: str | None = Field(default=None, description="Token expiration time (ISO format)") - - -class PasswordResetRequiredResponse(BaseModel): - reset_token: str = Field(..., description="Password reset token") - expires_at: str = Field(..., description="Token expiration time (ISO format)") - - class ResendVerificationRequest(BaseModel): email: EmailStr = Field(..., description="Email address to resend verification") diff --git a/backend/core/rbac.py b/backend/core/rbac.py index a105d0a..db9232f 100644 --- a/backend/core/rbac.py +++ b/backend/core/rbac.py @@ -31,8 +31,7 @@ async def get_user_role_level(user_id: str, db: AsyncSession) -> int: level = result.scalar_one_or_none() return int(level) if level is not None else 0 except Exception as e: - logger.error(f"Failed to get user role level: {e}") - return 0 + raise ServerException(f"Failed to get user role level: {e}") async def get_user_role_id(user_id: str, db: AsyncSession) -> str | None: @@ -47,8 +46,7 @@ async def get_user_role_id(user_id: str, db: AsyncSession) -> str | None: ) return result.scalar_one_or_none() except Exception as e: - logger.error(f"Failed to get user role id: {e}") - return None + raise ServerException(f"Failed to get user role id: {e}") async def user_has_role(user_id: str, role_id: str, db: AsyncSession) -> bool: @@ -62,8 +60,7 @@ async def user_has_role(user_id: str, role_id: str, db: AsyncSession) -> bool: ) return result.scalar_one_or_none() is not None except Exception as e: - logger.error(f"Failed to check user role assignment: {e}") - return False + raise ServerException(f"Failed to check user role assignment: {e}") async def get_user_attributes(user_id: str, db: AsyncSession) -> dict[str, bool]: @@ -95,8 +92,7 @@ async def get_user_attributes(user_id: str, db: AsyncSession) -> dict[str, bool] return attributes except Exception as e: - ServerException(f"Failed to get user attributes: {e}") - return {} + raise ServerException(f"Failed to get user attributes: {e}") async def check_user_has_super_role(user_id: str, db: AsyncSession) -> bool: @@ -111,8 +107,7 @@ async def check_user_has_super_role(user_id: str, db: AsyncSession) -> bool: user_roles = [row.name for row in result] return settings.DEFAULT_SUPER_ADMIN_ROLE in user_roles except Exception as e: - logger.error(f"Failed to check super role: {e}") - return False + raise ServerException(f"Failed to check super role: {e}") def require_permission(required_attributes: list[str]): diff --git a/backend/tests/api/auth/test_schema.py b/backend/tests/api/auth/test_schema.py index 023da12..7eca050 100644 --- a/backend/tests/api/auth/test_schema.py +++ b/backend/tests/api/auth/test_schema.py @@ -8,7 +8,6 @@ LoginResult, LogoutRequest, PasswordResetCooldownResponse, - PasswordResetRequiredResponse, ResetPasswordRequest, SessionResult, TokenResponse, @@ -197,25 +196,6 @@ def test_token_response_missing_fields(self): errors = exc_info.value.errors() assert len(errors) == 1 - def test_password_reset_required_response_valid(self): - """Test valid password reset required response data""" - data = {"reset_token": "test-reset-token", "expires_at": "2024-01-01T00:00:00Z"} - - password_reset_response = PasswordResetRequiredResponse(**data) - - assert password_reset_response.reset_token == "test-reset-token" - assert password_reset_response.expires_at == "2024-01-01T00:00:00Z" - - def test_password_reset_required_response_missing_fields(self): - """Test missing required fields""" - data = {"reset_token": "test-reset-token"} - - with pytest.raises(ValidationError) as exc_info: - PasswordResetRequiredResponse(**data) - - errors = exc_info.value.errors() - assert len(errors) == 1 - def test_logout_request_valid(self): """Test valid logout request data""" data = {"logout_all": True} @@ -340,33 +320,31 @@ def test_password_reset_cooldown_response_missing_field(self): def test_login_result_typing(self): """Test LoginResult TypedDict""" - user_data = { - "id": "test-user-id", - "first_name": "John", - "last_name": "Doe", - "email": "john.doe@example.com", - "phone": "+1234567890", - } + user = object() login_result: LoginResult = { - "user": user_data, + "user": user, "session_id": "test-session-id", "access_token": "test-access-token", + "csrf_token": "test-csrf-token", } - assert login_result["user"] == user_data + assert login_result["user"] is user assert login_result["session_id"] == "test-session-id" assert login_result["access_token"] == "test-access-token" + assert login_result["csrf_token"] == "test-csrf-token" def test_session_result_typing(self): """Test SessionResult TypedDict""" session_result: SessionResult = { "session_id": "test-session-id", "access_token": "test-access-token", + "csrf_token": "test-csrf-token", } assert session_result["session_id"] == "test-session-id" assert session_result["access_token"] == "test-access-token" + assert session_result["csrf_token"] == "test-csrf-token" def test_email_validation_edge_cases(self): """Test email validation edge cases""" diff --git a/backend/tests/core/test_rbac.py b/backend/tests/core/test_rbac.py index c70e4c7..61daf7d 100644 --- a/backend/tests/core/test_rbac.py +++ b/backend/tests/core/test_rbac.py @@ -18,6 +18,7 @@ from models.role_mapper import RoleMapper from models.roles import Roles from models.users import Users +from utils.custom_exception import ServerException class TestIsSuperAdminRoleName: @@ -54,10 +55,11 @@ async def test_returns_false_without_role( assert await check_user_has_super_role(test_user.id, test_db_session) is False @pytest.mark.asyncio - async def test_returns_false_on_error(self): + async def test_raises_server_exception_on_error(self): db = AsyncMock() db.execute.side_effect = RuntimeError("db down") - assert await check_user_has_super_role("user-1", db) is False + with pytest.raises(ServerException, match="Failed to check super role"): + await check_user_has_super_role("user-1", db) class TestGetUserAttributes: @@ -102,10 +104,11 @@ async def test_merges_attribute_values_with_or( assert attributes["view-users"] is True @pytest.mark.asyncio - async def test_returns_empty_on_error(self): + async def test_raises_server_exception_on_error(self): db = AsyncMock() db.execute.side_effect = RuntimeError("db down") - assert await get_user_attributes("user-1", db) == {} + with pytest.raises(ServerException, match="Failed to get user attributes"): + await get_user_attributes("user-1", db) class TestRequirePermission: diff --git a/nginx/docker-entrypoint.sh b/nginx/docker-entrypoint.sh index 44164cf..5ffcc39 100755 --- a/nginx/docker-entrypoint.sh +++ b/nginx/docker-entrypoint.sh @@ -53,9 +53,16 @@ EOF # Start cron service service cron start -# Auto-create whitelist.conf from example on first run (nginx/whitelist.conf on host) -if [ -f /etc/nginx/host/whitelist.conf.example ] && [ ! -f /etc/nginx/host/whitelist.conf ]; then - cp /etc/nginx/host/whitelist.conf.example /etc/nginx/host/whitelist.conf +# Auto-create whitelist.conf from example on first run (nginx/whitelist.conf on host). +# If a directory exists at that path (common Docker bind-mount mistake), replace it. +WHITELIST_PATH=/etc/nginx/host/whitelist.conf +WHITELIST_EXAMPLE=/etc/nginx/host/whitelist.conf.example +if [ -d "$WHITELIST_PATH" ]; then + echo "WARNING: $WHITELIST_PATH is a directory; replacing with file from example" + rm -rf "$WHITELIST_PATH" +fi +if [ -f "$WHITELIST_EXAMPLE" ] && [ ! -f "$WHITELIST_PATH" ]; then + cp "$WHITELIST_EXAMPLE" "$WHITELIST_PATH" fi # Process all template files @@ -67,4 +74,4 @@ for template in /etc/nginx/templates/*.conf; do done # Start nginx -exec nginx -g 'daemon off;' \ No newline at end of file +exec nginx -g 'daemon off;'