diff --git a/src/app/dashboard/admin/reports/[reportId]/page.tsx b/src/app/dashboard/admin/reports/[reportId]/page.tsx new file mode 100644 index 0000000..2740fad --- /dev/null +++ b/src/app/dashboard/admin/reports/[reportId]/page.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { ReportDetail } from "../components/report-detail"; + +export default function AdminReportDetailPage() { + const params = useParams(); + const reportId = params.reportId as string; + + const handleBackClick = () => { + window.history.back(); + }; + + return ( +
+ +
+ ); +} diff --git a/src/app/dashboard/admin/reports/components/report-detail.tsx b/src/app/dashboard/admin/reports/components/report-detail.tsx index 799943a..37c3e02 100644 --- a/src/app/dashboard/admin/reports/components/report-detail.tsx +++ b/src/app/dashboard/admin/reports/components/report-detail.tsx @@ -3,17 +3,59 @@ import { ArrowLeft } from "lucide-react" import { Button } from "@/components/ui/button" 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'; import Image from "next/image"; interface ReportDetailProps { - report: Report + reportId?: string; // New: for fetching from blockchain + report?: Report; // Optional: fallback to existing prop onBackClick: () => void } -export function ReportDetail({ report, onBackClick }: ReportDetailProps) { +export function ReportDetail({ reportId, report: propReport, onBackClick }: 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 || propReport; + + // 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 (
diff --git a/src/app/dashboard/components/report/ReportHeader.tsx b/src/app/dashboard/components/report/ReportHeader.tsx index f424333..0d9be97 100644 --- a/src/app/dashboard/components/report/ReportHeader.tsx +++ b/src/app/dashboard/components/report/ReportHeader.tsx @@ -18,7 +18,11 @@ export const ReportHeader: React.FC = ({ {title}
- {status ? : actionButton} + {status ? ( + + ) : ( + actionButton + )}
); }; diff --git a/src/app/dashboard/components/resuables/GetReport.tsx b/src/app/dashboard/components/resuables/GetReport.tsx new file mode 100644 index 0000000..891b544 --- /dev/null +++ b/src/app/dashboard/components/resuables/GetReport.tsx @@ -0,0 +1,473 @@ +"use client"; + +import { useEffect } from "react"; +import { motion } from "framer-motion"; +import { + FileText, + AlertTriangle, + Shield, + Clock, + User, + ExternalLink, + RefreshCw, + AlertCircle, + CheckCircle2, + XCircle, + ArrowLeft, + Bookmark, + BookmarkCheck +} from "lucide-react"; +import { useGetReport, UnifiedReport } from "@/hooks/useGetReport"; +import { Button } from "@/components/ui/button"; +import Image from "next/image"; +import poc1 from '../../../../../../public/poc1.svg'; +import poc2 from '../../../../../../public/poc2.svg'; +import poc3 from '../../../../../../public/poc3.svg'; +import bookmark from '../../../../../../public/bookmark.svg'; +import not_bookmark from '../../../../../../public/notbookmark.svg'; + +export interface GetReportProps { + reportId: string; + className?: string; + variant?: 'full' | 'compact' | 'dashboard'; + showSkeleton?: boolean; + onReportLoad?: (report: UnifiedReport) => void; + onError?: (error: string) => void; + // Dashboard-specific props + isBookmarked?: boolean; + onToggleBookmark?: () => void; + onBackClick?: () => void; + onApprovePayoutClick?: () => void; + onViewProjectClick?: () => void; + onRequestMoreInfoClick?: () => void; + onRejectReportClick?: () => void; + // Additional action buttons + actionButtons?: React.ReactNode; +} + +const GetReport: React.FC = ({ + reportId, + className = "", + variant = 'full', + showSkeleton = true, + onReportLoad, + onError, + isBookmarked = false, + onToggleBookmark, + onBackClick, + onApprovePayoutClick, + onViewProjectClick, + onRequestMoreInfoClick, + onRejectReportClick, + actionButtons +}) => { + const { report, loading, error, isEmpty, refetch } = useGetReport(reportId); + + // Notify parent when report loads + useEffect(() => { + if (report && onReportLoad) { + onReportLoad(report); + } + }, [report, onReportLoad]); + + // Notify parent of errors + useEffect(() => { + if (error && onError) { + onError(error); + } + }, [error, onError]); + + const handleRetry = () => { + refetch(); + }; + + const getSeverityColor = (severity: string) => { + switch (severity.toLowerCase()) { + case 'critical': return 'text-red-500 bg-red-500/10 border-red-500/20'; + case 'high': return 'text-orange-500 bg-orange-500/10 border-orange-500/20'; + case 'medium': return 'text-yellow-500 bg-yellow-500/10 border-yellow-500/20'; + case 'low': return 'text-blue-500 bg-blue-500/10 border-blue-500/20'; + case 'info': return 'text-gray-500 bg-gray-500/10 border-gray-500/20'; + default: return 'text-gray-500 bg-gray-500/10 border-gray-500/20'; + } + }; + + const getSeverityIcon = (severity: string) => { + switch (severity.toLowerCase()) { + case 'critical': return ; + case 'high': return ; + case 'medium': return ; + case 'low': return ; + case 'info': return ; + default: return ; + } + }; + + const formatTimestamp = (timestamp: number) => { + if (!timestamp) return "N/A"; + return new Date(timestamp * 1000).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + }; + + // Loading skeleton + if (loading && showSkeleton) { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); + } + + // Error state + if (error) { + return ( +
+
+ +

Error Loading Report

+

{error}

+ +
+
+ ); + } + + // Empty state + if (isEmpty) { + return ( +
+
+ +

No Report Found

+

Report with ID {reportId} could not be found or has no content.

+
+
+ ); + } + + // Main content + if (!report) { + return null; + } + + // Compact view for lists + if (variant === 'compact') { + return ( +
+
+

{report.title}

+
+ {getSeverityIcon(report.severity || 'Medium')} + {report.severity} +
+
+

{report.description}

+
+ ID: {report.id} + {formatTimestamp(report.created_at)} +
+
+ ); + } + + // Dashboard view that matches existing UI patterns + if (variant === 'dashboard') { + return ( +
+ {/* Header with back button and bookmark */} +
+ {onBackClick && ( + + )} +
+ +
+

+ {report.title} - {report.url} +

+ {onToggleBookmark && ( + + )} +
+ + {/* Metadata Grid */} +
+
+

Severity

+ + {report.severity} + +
+
+

CVSS Score

+ {report.cvssScore || 'N/A'} +
+
+

Vulnerable URL/Area

+ {report.url || 'N/A'} +
+
+

Vulnerable Form/Parameter

+ {report.vulnerableParameter || 'N/A'} +
+
+ + {/* Vulnerability Description */} + {report.vulnerabilityDescription && ( +
+

Vulnerability Description

+

{report.vulnerabilityDescription}

+
+ )} + + {/* Impact */} + {report.vulnerabilityImpact && ( +
+

Impact of Vulnerability

+

{report.vulnerabilityImpact}

+
+ )} + + {/* Steps to Reproduce */} + {report.stepsToReproduce && report.stepsToReproduce.length > 0 && ( +
+

Steps to reproduce

+
    + {report.stepsToReproduce.map((step, index) => ( +
  1. {step}
  2. + ))} +
+
+ )} + + {/* Proof of Concept */} + {report.proofOfConcept && report.proofOfConcept.length > 0 && ( +
+

Proof of Concept (PoC)

+
+
+ PoC Screenshot 1 +
+
+ PoC Screenshot 2 +
+
+ PoC Screenshot 3 +
+
+
+ )} + + {/* Mitigation Steps */} + {report.mitigationSteps && report.mitigationSteps.length > 0 && ( +
+

Mitigation Steps

+
    + {report.mitigationSteps.map((step, index) => ( +
  1. {step}
  2. + ))} +
+
+ )} + + {/* Action Buttons */} +
+ {onApprovePayoutClick && ( + + )} + {onViewProjectClick && ( + + )} + {onRequestMoreInfoClick && ( + + )} + {onRejectReportClick && ( + + )} + {actionButtons} +
+
+ ); + } + + // Default full view with modern card design + return ( + + {/* Header */} +
+
+

{report.title}

+

{report.description}

+
+
+ {getSeverityIcon(report.severity || 'Medium')} + {report.severity} +
+
+ + {/* Metadata */} +
+
+
+ + Report ID +
+

{report.id}

+
+ +
+
+ + Researcher +
+

{report.researcher_address}

+
+ +
+
+ + Created +
+

{formatTimestamp(report.created_at)}

+
+ +
+
+ + Project ID +
+

{report.project_id}

+
+
+ + {/* Summary */} + {report.summary && ( +
+

Executive Summary

+
+

{report.summary}

+
+
+ )} + + {/* Findings */} + {report.findings && report.findings.length > 0 && ( +
+

+ Findings ({report.findings.length}) +

+
+ {report.findings.map((finding, index) => ( +
+

{finding.vulnerability}

+
+
+ Impact: + {finding.impact} +
+
+ Recommendation: + {finding.recommendation} +
+ {finding.location && ( +
+ Location: + {finding.location} +
+ )} +
+
+ ))} +
+
+ )} + + {/* Footer */} +
+
+ Status: {report.status} + Updated: {formatTimestamp(report.updated_at)} +
+ {report.cid && ( + + + View on IPFS + + )} +
+
+ ); +}; + +export default GetReport; diff --git a/src/app/dashboard/components/resuables/WriteAReport.tsx b/src/app/dashboard/components/resuables/WriteAReport.tsx index dfdc818..920c308 100644 --- a/src/app/dashboard/components/resuables/WriteAReport.tsx +++ b/src/app/dashboard/components/resuables/WriteAReport.tsx @@ -11,6 +11,7 @@ import toast from "react-hot-toast"; import { writeContractWithStarknetJs } from "@/hooks/useBlockchain"; import { Account, byteArray, cairo } from "starknet"; import { useAccount } from "@starknet-react/core"; +import { has } from "lodash"; const ReactQuill = dynamic(() => import("react-quill"), { ssr: false }); @@ -30,9 +31,22 @@ const modules = { }; const formats = [ - "header", "font", "size", "bold", "italic", "underline", "strike", - "blockquote", "list", "bullet", "indent", "link", "image", "video", - "align", "code-block" + "header", + "font", + "size", + "bold", + "italic", + "underline", + "strike", + "blockquote", + "list", + "bullet", + "indent", + "link", + "image", + "video", + "align", + "code-block", ]; type WriteAReportProps = { @@ -41,7 +55,11 @@ type WriteAReportProps = { projectId?: number; }; -export default function WriteAReport({ isOpen, onClose, projectId }: WriteAReportProps) { +export default function WriteAReport({ + isOpen, + onClose, + projectId, +}: WriteAReportProps) { const [value, setValue] = useState(""); const [previewMode, setPreviewMode] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false); @@ -79,11 +97,13 @@ export default function WriteAReport({ isOpen, onClose, projectId }: WriteARepor }; const handleSubmit = async () => { - if (!title.trim() || !project.trim() || !value.trim()) { + if (!title.trim()) { toast.error("Please fill out all fields before submitting."); return; } + + setIsSubmitting(true); try { @@ -108,26 +128,44 @@ export default function WriteAReport({ isOpen, onClose, projectId }: WriteARepor setIpfsHash(hash); toast.success(`Report uploaded to IPFS! Hash: ${hash}`); + console.log({ + reportData, + projectId, + account, + hash, + }, 'hash'); + if (account && projectId) { toast.loading("Submitting to contract..."); - + const contractArgs = { project_id: cairo.uint256(projectId), - report_uri: byteArray.byteArrayFromString(hash) + report_uri: byteArray.byteArrayFromString(hash), }; - + const contractResult = await writeContractWithStarknetJs( account as Account, "submit_report", contractArgs ); - + + + console.log({ + contractResult + }, 'result'); + if (contractResult && contractResult.result && contractResult.status) { setContractReportId(reportId); setIsSubmitted(true); toast.dismiss(); - toast.success(`Report submitted to blockchain successfully! Report ID: ${reportId}`); + toast.success( + `Report submitted to blockchain successfully! Report ID: ${reportId}` + ); } else { + console.error( + "❌ WriteAReport: Contract transaction failed:", + contractResult + ); toast.dismiss(); toast.error("Contract transaction failed. Please try again."); } @@ -141,7 +179,7 @@ export default function WriteAReport({ isOpen, onClose, projectId }: WriteARepor console.error("Error submitting report:", error); toast.error("Failed to submit report. Please try again."); } finally { - setIsSubmitting(false); + // setIsSubmitting(false); } }; diff --git a/src/app/dashboard/project-owner/reports/[reportId]/page.tsx b/src/app/dashboard/project-owner/reports/[reportId]/page.tsx new file mode 100644 index 0000000..9ac9585 --- /dev/null +++ b/src/app/dashboard/project-owner/reports/[reportId]/page.tsx @@ -0,0 +1,496 @@ +"use client"; +import React, { useState } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { useGetReport } from "@/hooks/useGetReport"; +import { ArrowLeft, X } from "lucide-react"; +import { + poc1, + poc2, + dollars, + github, + calendar, + cairo, + python, + rust, + ts, +} from "../../../../../../public"; +import Image from "next/image"; +import { motion } from "framer-motion"; +import { + mainContainerVariants, + backButtonVariants, + titleContainerVariants, + titleVariants, + metadataContainerVariants, + metadataItemVariants, + sectionVariants, + sectionTitleVariants, + sectionContentVariants, + pocImageVariants, + buttonVariants, + modalItemVariants, +} from "../animations"; +import { Modal } from "@/app/dashboard/components/resuables/Modal"; + +const ReportPage: React.FC = () => { + const params = useParams(); + const router = useRouter(); + const [isModalOpen, setIsModalOpen] = useState(false); + + const reportId = Number(params.reportId) + + const { report, loading, error, refetch } = useGetReport(reportId || 0); + + const handleBackToReports = () => { + router.push('/dashboard/project-owner/reports'); + }; + + if (loading) { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+ ); + } + + // Show error state + if (error) { + return ( +
+
+

No Report Found

+

The requested report could not be found.

+ +
+
+ ); + } + + // Show empty state + if (!report) { + return ( +
+
+

Report Not Found

+

The requested report could not be found.

+ +
+
+ ); + } + + return ( + + + Back to Reports + + +
+ +
+ + {report.title || "Untitled Report"} + + +
+ ${report.cvssScore || "0"} +
+
+
+ + + +

Severity

+ + {report.severity || "Unknown"} + +
+ +

CVSS Score

+ {report.cvssScore || "N/A"} +
+ +

+ Vulnerable URL/Area: +

+ + {report.url || "N/A"} + +
+ +

+ Vulnerable Form/Parameter +

+ + {report.vulnerableParameter || "N/A"} + +
+
+
+ + + + + Vulnerability Description + +
+

{report.description || "No description provided."}

+
+
+ + + + Impact of Vulnerability + +
+

{report.vulnerabilityImpact || "No impact details provided."}

+
+
+ + + + Steps to Reproduce + +
+ {report.stepsToReproduce && report.stepsToReproduce.length > 0 ? ( +
    + {report.stepsToReproduce.map((step: string, index: number) => ( + + {step} + + ))} +
+ ) : ( +

No steps to reproduce provided.

+ )} +
+
+ + + + Proof of Concept (PoC) + +
+ {report.proofOfConcept && report.proofOfConcept.length > 0 ? ( + report.proofOfConcept.map((poc: string, index: number) => ( + + {`PoC + + )) + ) : ( +
+ + poc 1 + + + poc 2 + +
+ )} +
+
+ + + + Mitigation Steps for {report.title || "this vulnerability"} + +
+ {report.mitigationSteps && report.mitigationSteps.length > 0 ? ( +
    + {report.mitigationSteps.map((step: string, index: number) => ( + + {step} + + ))} +
+ ) : ( +

No mitigation steps provided.

+ )} +
+
+
+ +
+ + setIsModalOpen(false)}> + +
+

+ DG +

+

+ DeFi Guard +

+
+ +
+ + A decentralized finance (DeFi) protection tool that scans for + vulnerabilities in DeFi protocols and helps prevent hacks. + + + + DeFi + + + Storage + + + NFTs + + + +
+ dollars +

+ Prize Pool: $6,350,556 +

+
+
+ calendar +

+ Date of Expiry: 25-04-2025 +

+
+
+ +
+ github +

+ DeFi-Guard-Smartcontract +

+
+
+ github +

+ DeFi-Guard-Smartcontract +

+
+
+ +

+ Languages +

+
+
+
+ typescript +

TypeScript

+
+
+ python +

Python

+
+
+
+
+ cairo +

Cairo

+
+
+ rust +

Rust

+
+
+
+
+
+
+ ); +}; + +export default ReportPage; diff --git a/src/app/dashboard/project-owner/reports/sections/vulnerabilityreport.tsx b/src/app/dashboard/project-owner/reports/sections/vulnerabilityreport.tsx index a5740c8..18ee00f 100644 --- a/src/app/dashboard/project-owner/reports/sections/vulnerabilityreport.tsx +++ b/src/app/dashboard/project-owner/reports/sections/vulnerabilityreport.tsx @@ -33,10 +33,6 @@ const mainContainerVariants = { }, }; - - - - const VulnerabilityReport: React.FC = ({ setCurrentView, setVulnerabilityIndex }) => { const [selectedLanguages, setSelectedLanguages] = useState([]); const [selectedSeverity, setSelectedSeverity] = useState([]); diff --git a/src/app/dashboard/project-owner/reports/views.tsx b/src/app/dashboard/project-owner/reports/views.tsx index 1abefa4..4c5ae1c 100644 --- a/src/app/dashboard/project-owner/reports/views.tsx +++ b/src/app/dashboard/project-owner/reports/views.tsx @@ -6,48 +6,48 @@ import VulnerabilityReport from "./sections/vulnerabilityreport"; import Details from "./sections/details"; const Views = () => { - const [currentView, setCurrentView] = useState(0); - const [reportIndex, setReportIndex] = useState(null); - const [vulnerabilityIndex, setVulnerabilityIndex] = useState(null); + const [currentView, setCurrentView] = useState(0); + const [reportIndex, setReportIndex] = useState(null); + const [vulnerabilityIndex, setVulnerabilityIndex] = useState(null); - const renderCurrentView = () => { - switch (currentView) { - case 0: - return ( - >} - setReportIndex={setReportIndex} - /> - ); - case 1: - return ( - >} - setReportIndex={setReportIndex} - setVulnerabilityIndex={setVulnerabilityIndex} - /> - ); - case 2: - return ( -
>} - /> - ); - default: - return ( - >} - setReportIndex={setReportIndex} - /> - ); - } - }; + const renderCurrentView = () => { + switch (currentView) { + case 0: + return ( + >} + setReportIndex={setReportIndex} + /> + ); + case 1: + return ( + >} + setReportIndex={setReportIndex} + setVulnerabilityIndex={setVulnerabilityIndex} + /> + ); + case 2: + return ( +
>} + /> + ); + default: + return ( + >} + setReportIndex={setReportIndex} + /> + ); + } + }; - return <>{renderCurrentView()}; + return <>{renderCurrentView()}; }; -export default Views; \ No newline at end of file +export default Views; diff --git a/src/app/dashboard/researcher/projects/[projectId]/page.tsx b/src/app/dashboard/researcher/projects/[projectId]/page.tsx index 9212467..cf77f04 100644 --- a/src/app/dashboard/researcher/projects/[projectId]/page.tsx +++ b/src/app/dashboard/researcher/projects/[projectId]/page.tsx @@ -216,7 +216,7 @@ export default function ProjectDetailsPage({ params }: Props) { {/* Modals */} - + ); diff --git a/src/app/dashboard/researcher/reports/success/[id]/page.tsx b/src/app/dashboard/researcher/reports/success/[id]/page.tsx index ca6c00e..71010d4 100644 --- a/src/app/dashboard/researcher/reports/success/[id]/page.tsx +++ b/src/app/dashboard/researcher/reports/success/[id]/page.tsx @@ -1,7 +1,5 @@ -// pages/reports/success.tsx or app/reports/success/page.tsx (depending on your Next.js version) "use client"; -import { useState, useEffect} from 'react'; import { ReportLayout } from '../../ReportLayout'; import { useParams } from 'next/navigation'; import { ReportHeader } from '../../../../components/report/ReportHeader'; @@ -9,56 +7,17 @@ import { ReportInfoSection } from '../../../../components/report/ReportInfoSecti import { ReportTextSection } from '../../../../components/report/ReportTextSection'; import { ImageGallery } from '../../../../components/report/ImageGallery'; import { SuccessActions } from '../../../../components/report/ActionButtons'; +import { useGetReport } from '@/hooks/useGetReport'; -const getReportData = (id: string) => { - // This would typically be an API call - return { - id, - title: "Local File Inclusion (LFI) on Home Page – https://example.com/home", - status: "Fixed", - severity: "High", - cvssScore: "8.6", - vulnerableUrl: "https://example.com/home - Home Page", - vulnerableParam: "Broken Access Control", - description: "Attackers can exploit the Filename parameter to access sensitive files (e.g., /etc/passwd) by sending a crafted request, exposing critical server data.", - impact: "The vulnerability of Local File Inclusion (LFI) on the home page \"https://example.com/home\" can be attributed to the impact of the filename parameter. This vulnerability allows an attacker to manipulate the filename parameter in the URL to include arbitrary local files from the server.", - stepsToReproduce: ( -
    -
  1. Go to the Home Page (https://example.com/home).
  2. -
  3. Select any file from the selection section.
  4. -
  5. Intercept the request in the Burp Suite Proxy tool and send it to the request repeater tab in Burp Suite Proxy tool.
  6. -
- ), - mitigation: ( -
    -
  1. Implement input validation and sanitize user input to prevent the inclusion of unauthorized file paths or malicious input.
  2. -
  3. Avoid using user-supplied input directly in file inclusion functions. Instead, use a whitelist approach or predefined file mappings.
  4. -
- ), - pocImages: [ - { src: "/download3.svg", alt: "PoC 1" }, - { src: "/poc.svg", alt: "PoC 2" }, - { src: "/download.svg", alt: "PoC 3" } - ] - }; -}; const Success = () => { const params = useParams(); const reportId = params?.id as string; - const [loading, setLoading] = useState(true); - const [reportData, setReportData] = useState(null); - - useEffect(() => { - if (reportId) { - setTimeout(() => { - const data = getReportData(reportId); - setReportData(data); - setLoading(false); - }, 300); - } - }, [reportId]); + // Use the useGetReport hook to fetch full report data (blockchain + IPFS) + const { report, loading, error, isEmpty, refetch } = useGetReport(Number(reportId)); + + // Handle loading state if (loading) { return (
@@ -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) => ( -
    1. - {step} -
    2. - ))} + {(report.stepsToReproduce || stepsToReproduce).map( + (step: string, index: number) => ( +
    3. + {step} +
    4. + ) + )}
    @@ -144,15 +197,15 @@ export function ReportDetail({
    -

    - Mitigation Steps for Local File Inclusion -

    +

    Mitigation Steps

      - {mitigationSteps.map((step, index) => ( -
    1. - {step} -
    2. - ))} + {(report.mitigationSteps || mitigationSteps).map( + (step: string, index: number) => ( +
    3. + {step} +
    4. + ) + )}
    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; +}