From d5c057d6e4cd45cb44373b8e798f931f31b78fdf Mon Sep 17 00:00:00 2001 From: karthiksuki Date: Tue, 23 Jun 2026 00:46:55 +0530 Subject: [PATCH] fix: resolve cover_image relative URL validation error for self-hosted MinIO #9283 --- apps/api/plane/app/serializers/user.py | 7 +++++- apps/api/plane/settings/storage.py | 10 ++++++-- .../plane/tests/unit/settings/test_storage.py | 25 +++++++++++++++++++ apps/web/helpers/cover-image.helper.ts | 4 +++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/apps/api/plane/app/serializers/user.py b/apps/api/plane/app/serializers/user.py index aeef4ee28fb..6c0c2ac836f 100644 --- a/apps/api/plane/app/serializers/user.py +++ b/apps/api/plane/app/serializers/user.py @@ -26,7 +26,10 @@ def validate_last_name(self, value): class Meta: model = User # Exclude password field from the serializer - fields = [field.name for field in User._meta.fields if field.name != "password"] + fields = [field.name for field in User._meta.fields if field.name != "password"] + [ + "avatar_url", + "cover_image_url", + ] # Make all system fields and email read only read_only_fields = [ "id", @@ -53,6 +56,8 @@ class Meta: "is_email_verified", "is_active", "token_updated_at", + "avatar_url", + "cover_image_url", ] # If the user has already filled first name or last name then he is onboarded diff --git a/apps/api/plane/settings/storage.py b/apps/api/plane/settings/storage.py index e4a978bd2b1..f6693b46fef 100644 --- a/apps/api/plane/settings/storage.py +++ b/apps/api/plane/settings/storage.py @@ -22,7 +22,7 @@ def url(self, name, parameters=None, expire=None, http_method=None): """S3 storage class to generate presigned URLs for S3 objects""" - def __init__(self, request=None): + def __init__(self, request=None, is_server=False): # Get the AWS credentials and bucket name from the environment self.aws_access_key_id = os.environ.get("AWS_ACCESS_KEY_ID") # Use the AWS_SECRET_ACCESS_KEY environment variable for the secret key @@ -33,6 +33,7 @@ def __init__(self, request=None): self.aws_region = os.environ.get("AWS_REGION") # Use the AWS_S3_ENDPOINT_URL environment variable for the endpoint URL self.aws_s3_endpoint_url = os.environ.get("AWS_S3_ENDPOINT_URL") or os.environ.get("MINIO_ENDPOINT_URL") + self.aws_s3_public_endpoint_url = os.environ.get("MINIO_PUBLIC_ENDPOINT_URL") # Use the SIGNED_URL_EXPIRATION environment variable for the expiration time (default: 3600 seconds) self.signed_url_expiration = int(os.environ.get("SIGNED_URL_EXPIRATION", "3600")) @@ -42,13 +43,18 @@ def __init__(self, request=None): endpoint_protocol = "https" else: endpoint_protocol = request.scheme if request else "http" + endpoint_url = self.aws_s3_endpoint_url + if not is_server: + endpoint_url = self.aws_s3_public_endpoint_url or ( + f"{endpoint_protocol}://{request.get_host()}" if request else self.aws_s3_endpoint_url + ) # Create an S3 client for MinIO self.s3_client = boto3.client( "s3", aws_access_key_id=self.aws_access_key_id, aws_secret_access_key=self.aws_secret_access_key, region_name=self.aws_region, - endpoint_url=(f"{endpoint_protocol}://{request.get_host()}" if request else self.aws_s3_endpoint_url), + endpoint_url=endpoint_url, config=boto3.session.Config(signature_version="s3v4"), ) else: diff --git a/apps/api/plane/tests/unit/settings/test_storage.py b/apps/api/plane/tests/unit/settings/test_storage.py index 00856aeecb6..26b0d797811 100644 --- a/apps/api/plane/tests/unit/settings/test_storage.py +++ b/apps/api/plane/tests/unit/settings/test_storage.py @@ -51,6 +51,31 @@ def test_custom_expiration_multiple_values(self, mock_boto3): # Assert expiration is 300 assert storage.signed_url_expiration == 300 + @patch.dict( + os.environ, + { + "AWS_ACCESS_KEY_ID": "test-key", + "AWS_SECRET_ACCESS_KEY": "test-secret", + "AWS_S3_BUCKET_NAME": "test-bucket", + "AWS_S3_ENDPOINT_URL": "http://plane-minio:9000", + "MINIO_PUBLIC_ENDPOINT_URL": "http://localhost:9000", + "USE_MINIO": "1", + }, + clear=True, + ) + @patch("plane.settings.storage.boto3") + def test_minio_uses_public_endpoint_for_request_signed_urls(self, mock_boto3): + """Test MinIO signed URLs use a browser-reachable endpoint when configured""" + mock_boto3.client.return_value = Mock() + request = Mock() + request.scheme = "http" + request.get_host.return_value = "localhost:8000" + + S3Storage(request=request) + + call_kwargs = mock_boto3.client.call_args[1] + assert call_kwargs["endpoint_url"] == "http://localhost:9000" + @patch.dict( os.environ, { diff --git a/apps/web/helpers/cover-image.helper.ts b/apps/web/helpers/cover-image.helper.ts index 6ef7b2854c5..414e887d57e 100644 --- a/apps/web/helpers/cover-image.helper.ts +++ b/apps/web/helpers/cover-image.helper.ts @@ -276,6 +276,10 @@ export const handleCoverImageChange = async ( return; } + if (uploadConfig.isUserAsset && !newImage.startsWith("http")) { + return; + } + return { cover_image: newImage }; };