diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index ea696c4..a18c581 100755 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -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 diff --git a/backend/app/api/endpoints/datasets/datasets.py b/backend/app/api/endpoints/datasets/datasets.py index bd2bc73..3d412ad 100644 --- a/backend/app/api/endpoints/datasets/datasets.py +++ b/backend/app/api/endpoints/datasets/datasets.py @@ -1,3 +1,4 @@ +import os import uuid import mimetypes import pandas as pd @@ -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) diff --git a/backend/app/api/endpoints/users/role_check.py b/backend/app/api/endpoints/users/role_check.py index 3e64354..2ccd107 100644 --- a/backend/app/api/endpoints/users/role_check.py +++ b/backend/app/api/endpoints/users/role_check.py @@ -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) diff --git a/backend/test_presigned.py b/backend/test_presigned.py new file mode 100644 index 0000000..4f11c1f --- /dev/null +++ b/backend/test_presigned.py @@ -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()) diff --git a/frontend/next.config.ts b/frontend/next.config.ts index e9ffa30..ddb145a 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -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; diff --git a/frontend/src/components/file-upload/FileDownloadButton.tsx b/frontend/src/components/file-upload/FileDownloadButton.tsx index fbc5a98..34df14a 100644 --- a/frontend/src/components/file-upload/FileDownloadButton.tsx +++ b/frontend/src/components/file-upload/FileDownloadButton.tsx @@ -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 { fileID: string | null; @@ -30,7 +31,8 @@ const FileDownloadButton = React.forwardRef< ) => { const theToast = useToast(); - const handleDownload = async () => { + const handleDownload = async (e: React.MouseEvent) => { + e.preventDefault(); try { // If accessToken is "", show an error toast if (!accessToken || accessToken === "") { @@ -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", }); } @@ -92,6 +116,7 @@ const FileDownloadButton = React.forwardRef< return (