From e29ad5ee4247ee1cfa71d9f84b938b04d8761940 Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Thu, 24 Sep 2026 06:52:45 +0100 Subject: [PATCH 1/5] implemted the Optimize Evidence Images and Media Safely --- src/lib/evidence-validation.ts | 125 +++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 src/lib/evidence-validation.ts diff --git a/src/lib/evidence-validation.ts b/src/lib/evidence-validation.ts new file mode 100644 index 00000000..b00473ac --- /dev/null +++ b/src/lib/evidence-validation.ts @@ -0,0 +1,125 @@ +/** + * Safe evidence media validation utilities + * Validates remote sources, dimensions, formats, and ensures safe loading behavior + */ + +// Allowed image MIME types +const ALLOWED_IMAGE_MIME_TYPES = new Set([ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/webp', + 'image/avif', +]); + +// Allowed image file extensions +const ALLOWED_IMAGE_EXTENSIONS = new Set([ + '.jpg', '.jpeg', '.png', '.gif', '.webp', '.avif' +]); + +// Allowed video MIME types +const ALLOWED_VIDEO_MIME_TYPES = new Set([ + 'video/mp4', + 'video/webm', +]); + +// Allowed video file extensions +const ALLOWED_VIDEO_EXTENSIONS = new Set(['.mp4', '.webm']); + +// Allowed document MIME types (only PDFs for safety) +const ALLOWED_DOCUMENT_MIME_TYPES = new Set(['application/pdf']); +const ALLOWED_DOCUMENT_EXTENSIONS = new Set(['.pdf']); + +// Blocked URL schemes to prevent XSS +const BLOCKED_SCHEMES = new Set(['javascript:', 'data:', 'vbscript:', 'file:']); + +// Validate that a URL is safe to use +export function isValidMediaUrl(url: string): boolean { + try { + // Check for blocked schemes first + const lowerUrl = url.toLowerCase(); + for (const scheme of BLOCKED_SCHEMES) { + if (lowerUrl.startsWith(scheme)) { + return false; + } + } + + // For remote URLs, only allow HTTPS + const urlObj = new URL(url); + if (urlObj.protocol !== 'https:' && !url.startsWith('/')) { // Allow relative paths (local assets) + return false; + } + + return true; + } catch { + // If URL parsing fails, it's an invalid URL + return false; + } +} + +// Get file extension from URL +function getFileExtension(url: string): string { + try { + const pathname = new URL(url, window.location.origin).pathname; + const lastDot = pathname.lastIndexOf('.'); + if (lastDot === -1) return ''; + return pathname.slice(lastDot).toLowerCase(); + } catch { + return ''; + } +} + +// Validate image URL and format +export function isValidImageUrl(url: string): boolean { + if (!isValidMediaUrl(url)) return false; + const ext = getFileExtension(url); + return ALLOWED_IMAGE_EXTENSIONS.has(ext); +} + +// Validate video URL and format +export function isValidVideoUrl(url: string): boolean { + if (!isValidMediaUrl(url)) return false; + const ext = getFileExtension(url); + return ALLOWED_VIDEO_EXTENSIONS.has(ext); +} + +// Validate document URL and format +export function isValidDocumentUrl(url: string): boolean { + if (!isValidMediaUrl(url)) return false; + const ext = getFileExtension(url); + return ALLOWED_DOCUMENT_EXTENSIONS.has(ext); +} + +// Validate link URL (for external links) +export function isValidLinkUrl(url: string): boolean { + if (!isValidMediaUrl(url)) return false; + try { + const urlObj = new URL(url); + // Only allow HTTPS for external links + return urlObj.protocol === 'https:'; + } catch { + // If it's a relative URL, that's fine too + return url.startsWith('/'); + } +} + +// Media loading state hook return type +export interface MediaLoadingState { + isLoading: boolean; + hasError: boolean; + isLoaded: boolean; +} + +// Create initial media state +export function createInitialMediaState(): MediaLoadingState { + return { + isLoading: true, + hasError: false, + isLoaded: false, + }; +} + +// Helper to check if evidence type can contain media +export function isMediaType(type: string): boolean { + return ['image', 'video'].includes(type); +} \ No newline at end of file From 8f3990cd2350430152e4b398e73d4f96869173d0 Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Thu, 24 Sep 2026 06:53:36 +0100 Subject: [PATCH 2/5] implemted the Optimize Evidence scheme --- .../claim-verification/EvidenceViewer.tsx | 267 +++++++++++++++--- 1 file changed, 223 insertions(+), 44 deletions(-) diff --git a/src/components/features/claim-verification/EvidenceViewer.tsx b/src/components/features/claim-verification/EvidenceViewer.tsx index 10f97e83..ae05a742 100644 --- a/src/components/features/claim-verification/EvidenceViewer.tsx +++ b/src/components/features/claim-verification/EvidenceViewer.tsx @@ -1,18 +1,167 @@ 'use client'; -import { useState } from 'react'; +import { useState, useCallback } from 'react'; +import { Skeleton } from '@/components/ui/skeleton'; +import { + isValidMediaUrl, + isValidImageUrl, + isValidLinkUrl, + createInitialMediaState, + MediaLoadingState, + isMediaType +} from '@/lib/evidence-validation'; +import { Evidence, EvidenceType } from '@/app/types/claim'; +import { AlertCircle, ImageOff, Link2, FileText } from 'lucide-react'; -export function EvidenceViewer({ claimId: _claimId }: { claimId: string }) { +interface EvidenceViewerProps { + claimId: string; + evidence: Evidence[]; +} + +// Individual media component with loading and error states +function MediaRenderer({ + type, + value, + index +}: { + type: EvidenceType; + value: string; + index: number; +}) { + const [mediaState, setMediaState] = useState(createInitialMediaState()); + const [isValid, setIsValid] = useState(true); + + const handleLoad = useCallback(() => { + setMediaState({ + isLoading: false, + hasError: false, + isLoaded: true, + }); + }, []); + + const handleError = useCallback(() => { + setMediaState({ + isLoading: false, + hasError: true, + isLoaded: false, + }); + }, []); + + // Validate URL first + if (!isValidMediaUrl(value)) { + setIsValid(false); + } + + if (type === 'image' && isValid && isValidImageUrl(value)) { + return ( +
+ {mediaState.isLoading && ( + + )} + {`Evidence + {mediaState.hasError && ( +
+ + Failed to load image +
+ )} + {!isValid && ( +
+ + Invalid image source +
+ )} +
+ ); + } + + // Fallback for invalid images or other media types + if ((type === 'image' && !isValid) || (type === 'image' && !isValidImageUrl(value))) { + return ( +
+ + Invalid or unsupported image format +
+ ); + } + + return null; +} + +// Link renderer with validation +function LinkRenderer({ value, index }: { value: string; index: number }) { + const isValid = isValidLinkUrl(value); + + if (!isValid) { + return ( +
+ + Invalid link source blocked for security +
+ ); + } + + return ( + + + {value} + + ); +} + +// Text renderer +function TextRenderer({ value, index }: { value: string; index: number }) { + return ( +
+ +

+ {value} +

+
+ ); +} + +export function EvidenceViewer({ claimId: _claimId, evidence }: EvidenceViewerProps) { void _claimId; const [expanded, setExpanded] = useState(true); - // Assume evidence comes with claim fetch or separate endpoint - const evidence = [ - { type: 'link', value: 'https://example.com' }, - { type: 'text', value: 'Witness testimony text' }, - { type: 'image', value: '/evidence/img1.png' }, - ]; - return (
)} ); } -export default EvidenceViewer; +export default EvidenceViewer; \ No newline at end of file From ca6da2568ecc961a956fb2eb3ca38e07500bfd4c Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Thu, 24 Sep 2026 06:54:31 +0100 Subject: [PATCH 3/5] implemted the Optimize Evidence scheme --- src/app/(dashboard)/claims/[id]/page.tsx | 31 +++++++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/app/(dashboard)/claims/[id]/page.tsx b/src/app/(dashboard)/claims/[id]/page.tsx index 98297d34..bbb6c88d 100644 --- a/src/app/(dashboard)/claims/[id]/page.tsx +++ b/src/app/(dashboard)/claims/[id]/page.tsx @@ -1,11 +1,13 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { ClaimDetails } from '@/components/features/claim-verification/ClaimDetails'; import { EvidenceViewer } from '@/components/features/claim-verification/EvidenceViewer'; import { StakeForm } from '@/components/features/claim-verification/StakeForm'; import { VerificationActions } from '@/components/features/claim-verification/VerificationActions'; +import { getClaimById } from '@/app/lib/api'; +import { Claim } from '@/app/types/claim'; export default function ClaimDetailPage({ params, @@ -14,6 +16,25 @@ export default function ClaimDetailPage({ }) { const [stakeAmount, setStakeAmount] = useState(0); const [claimNotFound, setClaimNotFound] = useState(false); + const [claim, setClaim] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + if (params.id) { + setIsLoading(true); + getClaimById(params.id) + .then((data) => { + setClaim(data); + setIsLoading(false); + }) + .catch((err) => { + if (err.message === 'CLAIM_NOT_FOUND') { + setClaimNotFound(true); + } + setIsLoading(false); + }); + } + }, [params.id]); const handleStakeChange = (stake: string) => { const value = parseFloat(stake) || 0; @@ -143,7 +164,10 @@ export default function ClaimDetailPage({
- +
@@ -280,5 +304,4 @@ export default function ClaimDetailPage({ ); -} - +} \ No newline at end of file From 23f60b0c13c72a4520bb943502a70caa8e87fbfa Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Thu, 24 Sep 2026 06:54:58 +0100 Subject: [PATCH 4/5] implemted the Optimize Evidence Test --- .../__tests__/EvidenceViewer.new.test.tsx | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 src/components/features/claim-verification/__tests__/EvidenceViewer.new.test.tsx diff --git a/src/components/features/claim-verification/__tests__/EvidenceViewer.new.test.tsx b/src/components/features/claim-verification/__tests__/EvidenceViewer.new.test.tsx new file mode 100644 index 00000000..3108a116 --- /dev/null +++ b/src/components/features/claim-verification/__tests__/EvidenceViewer.new.test.tsx @@ -0,0 +1,181 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { EvidenceViewer } from '../EvidenceViewer'; +import type { Evidence } from '@/app/types/claim'; + +// Mock the lucide-react icons to avoid rendering issues in tests +jest.mock('lucide-react', () => ({ + AlertCircle: () =>
, + ImageOff: () =>
, + Link2: () =>
, + FileText: () =>
, +})); + +describe('EvidenceViewer - new functionality', () => { + const mockClaimId = 'claim-123'; + + it('shows no evidence state when evidence array is empty', () => { + render(); + expect(screen.getByTestId('no-evidence-state')).toBeInTheDocument(); + expect(screen.getByText('No evidence submitted for this claim')).toBeInTheDocument(); + }); + + it('renders valid image with loading state', async () => { + const validEvidence: Evidence[] = [ + { + id: '1', + type: 'image', + value: '/test-image.jpg', + createdAt: '2024-01-01T00:00:00Z', + }, + ]; + + render(); + + // Skeleton should be present initially (loading state) + expect(screen.getByTestId('image-skeleton-0')).toBeInTheDocument(); + + // After image loads, skeleton should be hidden and image visible + const img = screen.getByAltText('Evidence image 1'); + fireEvent.load(img); + + await waitFor(() => { + expect(screen.queryByTestId('image-skeleton-0')).not.toBeInTheDocument(); + }); + }); + + it('shows error state when image fails to load', async () => { + const brokenImageEvidence: Evidence[] = [ + { + id: '1', + type: 'image', + value: '/broken-image.jpg', + createdAt: '2024-01-01T00:00:00Z', + }, + ]; + + render(); + + const img = screen.getByAltText('Evidence image 1'); + fireEvent.error(img); + + await waitFor(() => { + expect(screen.getByTestId('image-error-0')).toBeInTheDocument(); + expect(screen.getByText('Failed to load image')).toBeInTheDocument(); + }); + }); + + it('blocks invalid URLs for security', () => { + const maliciousEvidence: Evidence[] = [ + { + id: '1', + type: 'link', + value: 'javascript:alert(1)', + createdAt: '2024-01-01T00:00:00Z', + }, + { + id: '2', + type: 'image', + value: 'data:image/png;base64,invalid', + createdAt: '2024-01-01T00:00:00Z', + }, + ]; + + render(); + + // Should show security error for invalid link + expect(screen.getByText('Invalid link source blocked for security')).toBeInTheDocument(); + + // Should show security error for invalid image + expect(screen.getByText('Invalid image source')).toBeInTheDocument(); + }); + + it('allows valid HTTPS links and blocks invalid schemes', () => { + const mixedEvidence: Evidence[] = [ + { + id: '1', + type: 'link', + value: 'https://trusted-source.com/evidence.pdf', + createdAt: '2024-01-01T00:00:00Z', + }, + { + id: '2', + type: 'link', + value: 'http://insecure-source.com', // HTTP should be blocked for external links + createdAt: '2024-01-01T00:00:00Z', + }, + ]; + + render(); + + // Valid HTTPS link should render normally + expect(screen.getByText('https://trusted-source.com/evidence.pdf')).toBeInTheDocument(); + + // Insecure HTTP link should be blocked + expect(screen.getByText('Invalid link source blocked for security')).toBeInTheDocument(); + }); + + it('renders text evidence properly', () => { + const textEvidence: Evidence[] = [ + { + id: '1', + type: 'text', + value: 'Witness statement about the incident', + createdAt: '2024-01-01T00:00:00Z', + }, + ]; + + render(); + expect(screen.getByText('Witness statement about the incident')).toBeInTheDocument(); + expect(screen.getByTestId('file-text')).toBeInTheDocument(); + }); + + it('handles video evidence with warning state', () => { + const videoEvidence: Evidence[] = [ + { + id: '1', + type: 'video', + value: 'https://example.com/video.mp4', + createdAt: '2024-01-01T00:00:00Z', + }, + ]; + + render(); + expect(screen.getByText('Video evidence requires manual verification - link available upon request')).toBeInTheDocument(); + }); + + it('validates image file extensions', () => { + const invalidFormatEvidence: Evidence[] = [ + { + id: '1', + type: 'image', + value: 'https://example.com/image.exe', // Executable, not an image + createdAt: '2024-01-01T00:00:00Z', + }, + ]; + + render(); + expect(screen.getByText('Invalid or unsupported image format')).toBeInTheDocument(); + }); + + it('maintains accessibility attributes', () => { + const testEvidence: Evidence[] = [ + { + id: '1', + type: 'image', + value: '/test.jpg', + createdAt: '2024-01-01T00:00:00Z', + }, + ]; + + render(); + + const toggleButton = screen.getByRole('button', { name: /evidence/i }); + expect(toggleButton).toHaveAttribute('aria-expanded', 'true'); + expect(toggleButton).toHaveAttribute('aria-controls', 'evidence-content'); + + // Error states have proper role="alert" + fireEvent.error(screen.getByAltText('Evidence image 1')); + expect(screen.getByRole('alert')).toBeInTheDocument(); + }); +}); \ No newline at end of file From 1765a3903c2711117454f52d3dc81468fa0152db Mon Sep 17 00:00:00 2001 From: codesailor4 Date: Thu, 24 Sep 2026 06:55:24 +0100 Subject: [PATCH 5/5] implemted the Optimize Evidence Test --- .../features/claim-details/EvidenceLinks.tsx | 63 ++++++++++++++----- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/src/components/features/claim-details/EvidenceLinks.tsx b/src/components/features/claim-details/EvidenceLinks.tsx index 8aeb88aa..dcad9572 100644 --- a/src/components/features/claim-details/EvidenceLinks.tsx +++ b/src/components/features/claim-details/EvidenceLinks.tsx @@ -1,6 +1,7 @@ import { Evidence } from "@/app/types/dispute"; -import { ExternalLink, FileText, LinkIcon } from "lucide-react"; +import { ExternalLink, FileText, LinkIcon, AlertCircle } from "lucide-react"; +import { isValidLinkUrl, isValidMediaUrl } from "@/lib/evidence-validation"; export const EvidenceLinks = ({ evidences }: { evidences: Evidence[] }) => { return ( @@ -10,20 +11,54 @@ export const EvidenceLinks = ({ evidences }: { evidences: Evidence[] }) => {

Evidence Links

- {evidences.map((evidence) => ( -
-
- -
-

{evidence.title}

-

{evidence.description}

-
-
- - View - + {evidences.length === 0 ? ( +
+ No evidence links submitted
- ))} + ) : ( + evidences.map((evidence) => { + const isValidUrl = isValidMediaUrl(evidence.url) && isValidLinkUrl(evidence.url); + + if (!isValidUrl) { + return ( +
+
+ +
+

{evidence.title}

+

Blocked: Invalid or unsafe URL

+
+
+
+ ); + } + + return ( +
+
+ +
+

{evidence.title}

+

{evidence.description}

+
+
+ + View + +
+ ); + }) + )}
);