@@ -67,14 +26,65 @@ const Success = () => {
);
}
- if (!reportData) {
+ // Handle simple error state
+ if (error) {
return (
-
Report not found
+
+
Report Not Found
+
+ The report with ID {reportId} could not be found.
+
+
+
+
+ );
+ }
+
+ // Handle empty state
+ if (isEmpty || !report) {
+ return (
+
+
+
Report Not Found
+
The report with ID {reportId} could not be found.
+
+
);
}
-
+
+ const reportData = {
+ id: report.id,
+ title: report.title || "",
+ status: report.status || "Unknown",
+ severity: report.severity || "Medium",
+ cvssScore: report.cvssScore?.toString() || "0",
+ vulnerableUrl: report.url || report.report_uri || "",
+ vulnerableParam: report.vulnerableParameter || "",
+ description: report.vulnerabilityDescription || report.description || "",
+ impact: report.vulnerabilityImpact || "",
+ stepsToReproduce: report.stepsToReproduce && report.stepsToReproduce.length > 0 ? report.stepsToReproduce : null,
+ mitigation: report.mitigationSteps && report.mitigationSteps.length > 0 ? report.mitigationSteps : null,
+ pocImages: report.proofOfConcept && report.proofOfConcept.length > 0 ?
+ report.proofOfConcept.map((poc, index) => ({
+ src: poc,
+ alt: `PoC ${index + 1}`
+ })) : null
+ };
+
+
+
return (
}>
@@ -90,40 +100,62 @@ const Success = () => {
vulnerableParam={reportData.vulnerableParam}
/>
-
-
-
+ {reportData.description && (
+
+
+
+ )}
-
-
-
+ {reportData.impact && (
+
+
+
+ )}
-
-
-
+ {reportData.stepsToReproduce && (
+
+
+ {reportData.stepsToReproduce.map((step, index) => (
+ {step}
+ ))}
+
+ }
+ />
+
+ )}
-
- }
- />
-
+ {reportData.pocImages && (
+
+ }
+ />
+
+ )}
-
-
-
+ {reportData.mitigation && (
+
+
+ {reportData.mitigation.map((step, index) => (
+ {step}
+ ))}
+
+ }
+ />
+
+ )}
);
diff --git a/src/app/dashboard/validator/reports/[reportId]/page.tsx b/src/app/dashboard/validator/reports/[reportId]/page.tsx
new file mode 100644
index 0000000..aba4cd5
--- /dev/null
+++ b/src/app/dashboard/validator/reports/[reportId]/page.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import { useParams } from "next/navigation";
+import { useState } from "react";
+import { ReportDetail } from "../components/report-detail";
+
+export default function ValidatorReportDetailPage() {
+ const params = useParams();
+ const reportId = params.reportId as string;
+ const [isBookmarked, setIsBookmarked] = useState(false);
+
+ const handleBackClick = () => {
+ window.history.back();
+ };
+
+ const handleApprovePayoutClick = () => {
+ console.log("Approve payout clicked for report:", reportId);
+ // Add your payout approval logic here
+ };
+
+ const handleViewProjectClick = () => {
+ console.log("View project clicked for report:", reportId);
+ // Add your view project logic here
+ };
+
+ const handleRequestMoreInfoClick = () => {
+ console.log("Request more info clicked for report:", reportId);
+ // Add your request more info logic here
+ };
+
+ const handleRejectReportClick = () => {
+ console.log("Reject report clicked for report:", reportId);
+ // Add your reject report logic here
+ };
+
+ return (
+
+ setIsBookmarked(!isBookmarked)}
+ onBackClick={handleBackClick}
+ onApprovePayoutClick={handleApprovePayoutClick}
+ onViewProjectClick={handleViewProjectClick}
+ onRequestMoreInfoClick={handleRequestMoreInfoClick}
+ onRejectReportClick={handleRejectReportClick}
+ />
+
+ );
+}
diff --git a/src/app/dashboard/validator/reports/components/report-detail.tsx b/src/app/dashboard/validator/reports/components/report-detail.tsx
index 05bc188..350c054 100644
--- a/src/app/dashboard/validator/reports/components/report-detail.tsx
+++ b/src/app/dashboard/validator/reports/components/report-detail.tsx
@@ -1,6 +1,6 @@
"use client";
-import { ArrowLeft, Bookmark, BookmarkCheck } from "lucide-react";
+import { ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
vulnerabilityDescription,
@@ -10,6 +10,7 @@ import {
} from "../data/mock-data";
import { getSeverityColor } from "../utils/helpers";
import type { Report } from "../types";
+import { useReportData } from "@/hooks/useReportData";
import poc1 from "../../../../../../public/poc1.svg";
import poc2 from "../../../../../../public/poc2.svg";
import poc3 from "../../../../../../public/poc3.svg";
@@ -18,7 +19,7 @@ import bookmark from "../../../../../../public/bookmark.svg";
import Image from "next/image";
interface ReportDetailProps {
- report: Report;
+ reportId?: string; // New: for fetching from blockchain
isBookmarked: boolean;
onToggleBookmark: () => void;
onBackClick: () => void;
@@ -29,7 +30,7 @@ interface ReportDetailProps {
}
export function ReportDetail({
- report,
+ reportId,
isBookmarked,
onToggleBookmark,
onBackClick,
@@ -38,6 +39,52 @@ export function ReportDetail({
onRequestMoreInfoClick,
onRejectReportClick,
}: ReportDetailProps) {
+ // Fetch report data from blockchain/IPFS if reportId is provided
+ const {
+ reportData: fetchedReport,
+ loading,
+ error,
+ } = useReportData(reportId || "");
+
+ // Use fetched data if available, otherwise fall back to prop
+ const report = fetchedReport;
+
+ // Show loading state while fetching
+ if (reportId && loading) {
+ return (
+
+ );
+ }
+
+ // Show error state if fetching failed
+ if (reportId && error) {
+ return (
+
+
+
+ Error Loading Report
+
+
{error}
+
+
+ );
+ }
+
+ // Return null if no report data available
+ if (!report) {
+ return null;
+ }
return (
@@ -97,22 +144,28 @@ export function ReportDetail({
Vulnerability Description
-
{vulnerabilityDescription}
+
+ {report.vulnerabilityDescription || vulnerabilityDescription}
+
Impact of Vulnerability
-
{vulnerabilityImpact}
+
+ {report.vulnerabilityImpact || vulnerabilityImpact}
+
Steps to reproduce
- {stepsToReproduce.map((step, index) => (
- -
- {step}
-
- ))}
+ {(report.stepsToReproduce || stepsToReproduce).map(
+ (step: string, index: number) => (
+ -
+ {step}
+
+ )
+ )}
@@ -144,15 +197,15 @@ export function ReportDetail({
-
- Mitigation Steps for Local File Inclusion
-
+
Mitigation Steps
- {mitigationSteps.map((step, index) => (
- -
- {step}
-
- ))}
+ {(report.mitigationSteps || mitigationSteps).map(
+ (step: string, index: number) => (
+ -
+ {step}
+
+ )
+ )}
diff --git a/src/app/dashboard/validator/reports/components/reports-page.tsx b/src/app/dashboard/validator/reports/components/reports-page.tsx
index d49590f..478f2dc 100644
--- a/src/app/dashboard/validator/reports/components/reports-page.tsx
+++ b/src/app/dashboard/validator/reports/components/reports-page.tsx
@@ -10,7 +10,6 @@ import { RejectReportModal } from "./modals/reject-report-modal";
import { PayoutProcessingModal } from "./modals/payout-processing-modal";
import { PayoutSuccessModal } from "./modals/payout-success-modal";
import type { Report } from "../types";
-import { PayoutFailedModal } from "./modals/payout-failed-modal";
import { RequestSentModal } from "./modals/request-sent-modal";
import { RejectSuccesModal } from "./modals/reject-success-modal";
@@ -107,7 +106,6 @@ function ReportsPage() {
/>
) : (
{
try {
- const response = await fetch(
- `${process.env.NEXT_PUBLIC_PINATA_GATEWAY_URL}${cid}?pinataGatewayToken=${process.env.NEXT_PUBLIC_PINATA_GATEWAY_TOKEN}`
- );
- const data = await response.json();
+ const cleanCid = cid.replace(/^ipfs\//, '').replace(/^\//, '');
+ const response = await fetch(`https://gateway.pinata.cloud/ipfs/${cleanCid}`, {
+ method: 'GET',
+ headers: {
+ 'Accept': 'application/json',
+ },
- return { ...data, cid: cid };
+ });
+ if (!response.ok) {
+ console.error(`Pinata gateway error: ${response.status}`);
+ return null;
+ }
+
+ const data = await response.json();
+ return { ...data, cid: cleanCid };
+
} catch (error) {
- console.error(`Error fetching data for CID ${cid}:`, error);
return null;
}
};
+
+
+
+
+
export const uploadImageToPinata = async (file: File): Promise => {
const formData = new FormData();
formData.append("file", file);
diff --git a/src/hooks/useGetReport.ts b/src/hooks/useGetReport.ts
new file mode 100644
index 0000000..02c4778
--- /dev/null
+++ b/src/hooks/useGetReport.ts
@@ -0,0 +1,212 @@
+"use client";
+
+import { useEffect, useState, useMemo, useCallback } from "react";
+import { FORTICHAIN_ABI } from "@/app/abi/fortichain-abi";
+import { useContractFetch, fetchContentFromIPFS } from "@/hooks/useBlockchain";
+
+export interface UnifiedReport {
+ id: string;
+ report_uri: string;
+ researcher_address: string;
+ project_id: string;
+ status: string;
+ created_at: number;
+ updated_at: number;
+ title?: string;
+ description?: string;
+ severity?: "Critical" | "High" | "Medium" | "Low";
+ cvssScore?: number;
+ url?: string;
+ vulnerableParameter?: string;
+ vulnerabilityDescription?: string;
+ vulnerabilityImpact?: string;
+ stepsToReproduce?: string[];
+ mitigationSteps?: string[];
+ proofOfConcept?: string[];
+ findings?: Array<{
+ vulnerability: string;
+ impact: string;
+ recommendation: string;
+ codeSnippet?: string;
+ location?: string;
+ }>;
+ summary?: string;
+ methodology?: string;
+ tools_used?: string[];
+ timeline?: {
+ started: string;
+ completed: string;
+ };
+ researcher_info?: {
+ name: string;
+ experience: string;
+ specialization: string[];
+ };
+ cid?: string;
+}
+
+export interface UseGetReportReturn {
+ report: UnifiedReport | null;
+ loading: boolean;
+ error: string | null;
+ isEmpty: boolean;
+ refetch: () => void;
+}
+
+const createBlockchainReport = (reportData: any, normalizedId: string) => ({
+ id: reportData.id?.toString() || normalizedId,
+ report_uri: reportData.report_uri || "",
+ researcher_address: reportData.researcher_address || "",
+ project_id: reportData.project_id?.toString() || "",
+ status: "",
+ created_at: reportData.created_at ? Number(reportData.created_at) : 0,
+ updated_at: reportData.updated_at ? Number(reportData.updated_at) : 0,
+});
+
+const transformIpfsContent = (
+ ipfsContent: any,
+ blockchainReport: any
+): UnifiedReport => ({
+ ...blockchainReport,
+ title: ipfsContent.reportName || "Untitled Report",
+ description: ipfsContent.description || "",
+ severity: ipfsContent.severity || "Medium",
+ cvssScore: ipfsContent.cvssScore || 0,
+ url: ipfsContent.url || "",
+ status: ipfsContent.status || "",
+ vulnerableParameter: ipfsContent.vulnerableParameter || "",
+ vulnerabilityDescription: ipfsContent.vulnerabilityDescription || "",
+ vulnerabilityImpact: ipfsContent.vulnerabilityImpact || "",
+ stepsToReproduce: ipfsContent.stepsToReproduce || [],
+ mitigationSteps: ipfsContent.mitigationSteps || [],
+ proofOfConcept: ipfsContent.proofOfConcept || [],
+ findings: ipfsContent.findings || [],
+ summary: ipfsContent.summary || "",
+ methodology: ipfsContent.methodology || "",
+ tools_used: ipfsContent.tools_used || [],
+ timeline: ipfsContent.timeline,
+ researcher_info: ipfsContent.researcher_info,
+ cid: ipfsContent.cid,
+});
+
+export function useGetReport(reportId: string | number): UseGetReportReturn {
+ const [state, setState] = useState<{
+ report: UnifiedReport | null;
+ loading: boolean;
+ error: string | null;
+ isEmpty: boolean;
+ }>({
+ report: null,
+ loading: true,
+ error: null,
+ isEmpty: false,
+ });
+
+ const normalizedId = useMemo(
+ () => (typeof reportId === "number" ? reportId.toString() : reportId),
+ [reportId]
+ );
+
+ const {
+ readData: reportData,
+ readIsError: contractError,
+ readIsLoading: contractLoading,
+ readError: contractErrorDetails,
+ dataRefetch: refetchContract,
+ } = useContractFetch(FORTICHAIN_ABI, "get_report", [normalizedId]);
+
+ const refetch = useCallback(() => {
+ setState((prev) => ({
+ ...prev,
+ loading: true,
+ error: null,
+ isEmpty: false,
+ }));
+ refetchContract();
+ }, [refetchContract]);
+
+ const setReportData = useCallback(
+ (
+ report: UnifiedReport | null,
+ loading: boolean,
+ error: string | null,
+ isEmpty: boolean
+ ) => {
+ setState({ report, loading, error, isEmpty });
+ },
+ []
+ );
+
+ // Memoized blockchain report to prevent unnecessary recalculations
+ const blockchainReport = useMemo(() => {
+ if (!reportData) return null;
+ return createBlockchainReport(reportData, normalizedId);
+ }, [reportData, normalizedId]);
+
+ useEffect(() => {
+ if (!reportData || contractError || contractLoading) return;
+
+ const fetchReportContent = async () => {
+ try {
+ if (!blockchainReport?.report_uri) {
+ setReportData(
+ null,
+ false,
+ "Report URI not found on blockchain",
+ true
+ );
+ return;
+ }
+
+ const ipfsContent = await fetchContentFromIPFS(
+ blockchainReport.report_uri
+ );
+
+ if (!ipfsContent) {
+ setReportData(
+ null,
+ false,
+ "Report content not available from IPFS",
+ true
+ );
+ return;
+ }
+
+ const unifiedReport = transformIpfsContent(
+ ipfsContent,
+ blockchainReport
+ );
+ setReportData(unifiedReport, false, null, false);
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Unknown error occurred";
+ setReportData(null, false, errorMessage, false);
+ }
+ };
+
+ fetchReportContent();
+ }, [
+ reportData,
+ contractError,
+ contractLoading,
+ blockchainReport,
+ setReportData,
+ ]);
+
+
+
+ // Contract error handling effect
+ useEffect(() => {
+ if (contractError && contractErrorDetails) {
+ const errorMessage =
+ contractErrorDetails.message ||
+ "Failed to fetch report from blockchain";
+ setReportData(null, false, errorMessage, false);
+ }
+ }, [contractError, contractErrorDetails, setReportData]);
+
+ return {
+ ...state,
+ refetch,
+ };
+}
diff --git a/src/hooks/useReportData.ts b/src/hooks/useReportData.ts
new file mode 100644
index 0000000..7e6aa75
--- /dev/null
+++ b/src/hooks/useReportData.ts
@@ -0,0 +1,143 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { FORTICHAIN_ABI } from "@/app/abi/fortichain-abi";
+import { useContractFetch, fetchContentFromIPFS } from "@/hooks/useBlockchain";
+
+// Simple hook that works with existing dashboard interfaces
+export function useReportData(reportId: string) {
+ const [reportData, setReportData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ // Fetch report from blockchain
+ const {
+ readData: blockchainData,
+ readIsError: contractError,
+ readIsLoading: contractLoading,
+ readError: contractErrorDetails,
+ dataRefetch: refetchContract
+ } = useContractFetch(FORTICHAIN_ABI, "get_report", [reportId]);
+
+ useEffect(() => {
+ const fetchCompleteReport = async () => {
+ console.log("🔍 useReportData: Starting fetch for reportId:", reportId);
+
+ if (!blockchainData || contractError || contractLoading) {
+ console.log("⏳ useReportData: Waiting for blockchain data...", {
+ hasBlockchainData: !!blockchainData,
+ contractError,
+ contractLoading
+ });
+ return;
+ }
+
+ try {
+ console.log("📦 useReportData: Blockchain data received:", blockchainData);
+ setLoading(true);
+ setError(null);
+
+ // Get IPFS hash from blockchain
+ const ipfsHash = blockchainData.report_uri;
+ console.log("🔗 useReportData: IPFS hash from blockchain:", ipfsHash);
+
+ if (!ipfsHash) {
+ console.error("❌ useReportData: No IPFS hash found in blockchain data");
+ setError("Report URI not found on blockchain");
+ setLoading(false);
+ return;
+ }
+
+ // Fetch content from IPFS
+ console.log("📡 useReportData: Fetching content from IPFS...");
+ const ipfsContent = await fetchContentFromIPFS(ipfsHash);
+ console.log("📄 useReportData: IPFS content received:", ipfsContent);
+
+ if (!ipfsContent) {
+ console.error("❌ useReportData: Failed to fetch IPFS content");
+ setError("Failed to fetch report content from IPFS");
+ setLoading(false);
+ return;
+ }
+
+ // Combine blockchain and IPFS data
+ console.log("🔄 useReportData: Combining blockchain and IPFS data...");
+ const completeReport = {
+ // Blockchain data
+ id: blockchainData.id?.toString() || reportId,
+ researcher_address: blockchainData.researcher_address || "",
+ project_id: blockchainData.project_id?.toString() || "",
+ status: blockchainData.status?.toString() || "",
+ created_at: blockchainData.created_at ? Number(blockchainData.created_at) : 0,
+ updated_at: blockchainData.updated_at ? Number(blockchainData.updated_at) : 0,
+
+ // IPFS content - mapped to existing dashboard interfaces
+ title: ipfsContent.title || "Untitled Report",
+ projectName: ipfsContent.projectName || ipfsContent.project_name || "Unknown Project",
+ reviewedBy: ipfsContent.reviewedBy || ipfsContent.reviewed_by || "Pending",
+ submittedBy: ipfsContent.submittedBy || ipfsContent.submitted_by || blockchainData.researcher_address,
+ bounty: ipfsContent.bounty || ipfsContent.researcher_reward || "0",
+ researcherReward: ipfsContent.researcherReward || ipfsContent.researcher_reward || "0",
+ validatorReward: ipfsContent.validatorReward || ipfsContent.validator_reward || "0",
+ severity: ipfsContent.severity || "Medium",
+ cvssScore: ipfsContent.cvssScore || ipfsContent.cvss_score || 0,
+ url: ipfsContent.url || ipfsContent.vulnerable_url || "",
+ vulnerableUrl: ipfsContent.vulnerableUrl || ipfsContent.vulnerable_url || "",
+ vulnerableParameter: ipfsContent.vulnerableParameter || ipfsContent.vulnerable_parameter || "",
+
+ // Additional content
+ description: ipfsContent.description || "",
+ vulnerabilityDescription: ipfsContent.vulnerabilityDescription || ipfsContent.vulnerability_description || "",
+ vulnerabilityImpact: ipfsContent.vulnerabilityImpact || ipfsContent.vulnerability_impact || "",
+ stepsToReproduce: ipfsContent.stepsToReproduce || ipfsContent.steps_to_reproduce || [],
+ mitigationSteps: ipfsContent.mitigationSteps || ipfsContent.mitigation_steps || [],
+ proofOfConcept: ipfsContent.proofOfConcept || ipfsContent.proof_of_concept || [],
+ findings: ipfsContent.findings || [],
+ summary: ipfsContent.summary || "",
+ methodology: ipfsContent.methodology || "",
+ tools_used: ipfsContent.tools_used || [],
+ timeline: ipfsContent.timeline,
+ researcher_info: ipfsContent.researcher_info,
+
+ // IPFS metadata
+ report_uri: ipfsHash,
+ cid: ipfsContent.cid || ipfsHash
+ };
+
+ setReportData(completeReport);
+ setLoading(false);
+ console.log("✅ useReportData: Report data successfully combined and set:", completeReport);
+
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
+ console.error("❌ useReportData: Error during fetch process:", error);
+ setError(errorMessage);
+ setLoading(false);
+ }
+ };
+
+ fetchCompleteReport();
+ }, [blockchainData, contractError, contractLoading, reportId]);
+
+ // Handle contract errors
+ useEffect(() => {
+ if (contractError && contractErrorDetails) {
+ const errorMessage = contractErrorDetails.message || "Failed to fetch report from blockchain";
+ setError(errorMessage);
+ setLoading(false);
+ }
+ }, [contractError, contractErrorDetails]);
+
+ const refetch = () => {
+ setLoading(true);
+ setError(null);
+ refetchContract();
+ };
+
+ return {
+ reportData,
+ loading,
+ error,
+ refetch
+ };
+}
diff --git a/src/types/report.ts b/src/types/report.ts
new file mode 100644
index 0000000..04fdfa6
--- /dev/null
+++ b/src/types/report.ts
@@ -0,0 +1,53 @@
+export interface Report {
+ id: string;
+ report_uri: string;
+ researcher_address: string;
+ project_id: string;
+ status: string;
+ created_at: number;
+ updated_at: number;
+}
+
+export interface ReportContent {
+ title: string;
+ description: string;
+ severity: 'Critical' | 'High' | 'Medium' | 'Low' | 'Info';
+ category: string;
+ findings: {
+ vulnerability: string;
+ impact: string;
+ recommendation: string;
+ codeSnippet?: string;
+ location?: string;
+ }[];
+ summary: string;
+ methodology: string;
+ tools_used?: string[];
+ timeline?: {
+ started: string;
+ completed: string;
+ };
+ researcher_info?: {
+ name: string;
+ experience: string;
+ specialization: string[];
+ };
+ cid?: string;
+}
+
+export interface GetReportState {
+ report: Report | null;
+ reportContent: ReportContent | null;
+ loading: boolean;
+ error: string | null;
+ isEmpty: boolean;
+}
+
+export interface GetReportProps {
+ reportId: string;
+ className?: string;
+ onReportLoad?: (report: Report, content: ReportContent) => void;
+ onError?: (error: string) => void;
+ showSkeleton?: boolean;
+ compact?: boolean;
+}