Skip to content
Open
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
1 change: 1 addition & 0 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ services:
environment:
MINIO_ROOT_USER: ${MINIO_USERNAME}
MINIO_ROOT_PASSWORD: ${MINIO_PASSWORD}
MINIO_API_CORS_ALLOW_ORIGIN: "*"
command: server /data/s3 --console-address ":9001"
restart: unless-stopped
network_mode: host
10 changes: 10 additions & 0 deletions backend/app/api/endpoints/datasets/datasets.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import uuid
import mimetypes
import pandas as pd
Expand Down Expand Up @@ -27,6 +28,15 @@
def get_presigned_url(filename: str, current_user: dict = Depends(get_current_user)) -> PresignedURLResponse:
storage_service = get_storage_service()
user_id = str(current_user.get("_id"))

# Check if filename is an existing file_id ObjectId in files_collection
if ObjectId.is_valid(filename):
file_doc = files_collection.find_one({"_id": ObjectId(filename)})
if file_doc and file_doc.get("file_location"):
file_location = file_doc["file_location"]
download_url = storage_service.generate_download_url(file_location)
return PresignedURLResponse(upload_url=download_url, object_name=file_location)

url, object_name = storage_service.generate_presigned_url(
filename=filename, user_id=user_id)
return PresignedURLResponse(upload_url=url, object_name=object_name)
Expand Down
26 changes: 20 additions & 6 deletions backend/app/api/endpoints/users/role_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,27 @@ def check_user_role_access(request: RoleCheckRequest, fastapi_request: Request,
raise HTTPException(status_code=404, detail="User not found")

# Get user's role
role_id = user.get("role_id")
role_ids = user.get("role_id")
role_name = "user"
if role_id:
role = get_role_by_id(role_id)
if role and role.get("role_name"):
role_name = role["role_name"]
logger.debug("User role resolved: role_id=%s role_name=%s", role_id, role_name)
if role_ids:
# Handle both list of IDs and single ID cases
if not isinstance(role_ids, list):
role_ids = [role_ids]

roles_found = []
for r_id in role_ids:
role = get_role_by_id(r_id)
if role and role.get("role_name"):
roles_found.append(role["role_name"])

# Select the most privileged role if multiple exist
if "superadmin" in roles_found:
role_name = "superadmin"
elif "admin" in roles_found:
role_name = "admin"
elif roles_found:
role_name = roles_found[0]
logger.debug("User role resolved: role_ids=%s role_name=%s", role_ids, role_name)

# Find matching endpoint access rule
endpoint_access = find_matching_endpoint_access(role_name, request.path)
Expand Down
7 changes: 7 additions & 0 deletions backend/test_presigned.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import requests
import os

token = "114273893325363479564" # from the logs
headers = {"Authorization": f"Bearer {token}"}
resp = requests.get("http://localhost:8000/presignedURL?filename=test.csv", headers=headers)
print(resp.json())
9 changes: 8 additions & 1 deletion frontend/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
/* config options here */
async rewrites() {
return [
{
source: "/s3local/:path*",
destination: "http://localhost:9000/:path*",
},
];
},
};

export default nextConfig;
75 changes: 50 additions & 25 deletions frontend/src/components/file-upload/FileDownloadButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Button, ButtonProps } from "../ui/button";
import { Download } from "lucide-react";
import { cn } from "@/lib/utils";
import { useToast } from "../hooks/use-toast";
import { getPresignedUrl } from "@/lib/hey-api/client/sdk.gen";

export interface FileDownloadButtonProps extends Omit<ButtonProps, "onClick"> {
fileID: string | null;
Expand Down Expand Up @@ -30,7 +31,8 @@ const FileDownloadButton = React.forwardRef<
) => {
const theToast = useToast();

const handleDownload = async () => {
const handleDownload = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
try {
// If accessToken is "", show an error toast
if (!accessToken || accessToken === "") {
Expand All @@ -54,36 +56,58 @@ const FileDownloadButton = React.forwardRef<
return;
}

// // Get the download URL from the API
// const response = await getNimbusFileDownloadUrl({
// path: {
// fileId: fileID,
// },
// headers: {
// Authorization: `Bearer ${accessToken}`,
// },
// });
const response = await getPresignedUrl({
query: {
filename: fileID,
},
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});

const response = {
data: "https://example.com/file.csv",
};
const presignedUrlData = response.data as { upload_url?: string; object_name?: string };
const rawUrl = presignedUrlData?.upload_url;
const objectName = presignedUrlData?.object_name;

if (response.data) {
window.open(response.data as string, "_blank");
} else {
theToast.toast({
title: "Error",
description: "Failed to get file download URL",
variant: "destructive",
});
console.error("No download URL received");
throw new Error("No download URL received");
if (!rawUrl && !objectName) {
throw new Error("Failed to get file URL");
}

// Clean query parameters to avoid signature mismatch on GET requests
let cleanUrl = rawUrl ? rawUrl.split("?")[0] : "";
if (!cleanUrl && objectName) {
cleanUrl = `http://localhost:9000/uploads/${objectName}`;
}

// Route through /s3local proxy for same-origin fetch in browser
if (typeof window !== "undefined") {
cleanUrl = cleanUrl.replace(
/^http:\/\/(localhost|127\.0\.0\.1):9000/,
`${window.location.origin}/s3local`
);
}

const fileResponse = await fetch(cleanUrl);
if (!fileResponse.ok) {
throw new Error(`Failed to fetch file (status ${fileResponse.status})`);
}

const blob = await fileResponse.blob();
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = blobUrl;
const fileName = downloadName.endsWith(".csv") ? downloadName : `${downloadName}.csv`;
a.download = fileName;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(blobUrl);
} catch (error) {
console.error("Error getting file download URL:", error);
console.error("Error downloading file:", error);
theToast.toast({
title: "Error",
description: "Failed to get file download URL",
description: error instanceof Error ? error.message : "Failed to download file",
variant: "destructive",
});
}
Expand All @@ -92,6 +116,7 @@ const FileDownloadButton = React.forwardRef<
return (
<Button
ref={ref}
type="button"
variant={variant}
size={size}
className={cn("w-full", className)}
Expand Down
25 changes: 20 additions & 5 deletions frontend/src/components/file-upload/FileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,28 @@ export const FileUploader = forwardRef<
throw new Error("Failed to get presigned URL");
}

// Route presigned URL through Next.js /s3local proxy to avoid browser CORS/PNA issues
let targetUrl = upload_url;
if (typeof window !== "undefined") {
targetUrl = targetUrl.replace(
/^http:\/\/(localhost|127\.0\.0\.1):9000/,
`${window.location.origin}/s3local`
);
}

const uploadHeaders: Record<string, string> = {};
if (file.type) {
uploadHeaders["Content-Type"] = file.type;
}

// Now upload the file using the presigned URL
const uploadResponse = await fetch(upload_url, {
const uploadResponse = await fetch(targetUrl, {
method: "PUT",
body: file,
headers: {
"Content-Type": file.type,
},
headers: uploadHeaders,
});
if (!uploadResponse.ok) {
throw new Error("Upload failed");
throw new Error(`Upload failed with status ${uploadResponse.status}`);
}
console.log("Upload response:", uploadResponse);

Expand Down Expand Up @@ -233,6 +245,9 @@ export const FileUploader = forwardRef<
};
} catch (error) {
console.error("Upload error:", error);
if (error instanceof Error && error.message === "Failed to fetch") {
toast.error("Upload connection failed. If you are using Brave, please try disabling Brave Shields (click the orange lion icon in the URL bar and toggle it off).");
}
throw error;
}
},
Expand Down