diff --git a/public/_redirects b/public/_redirects index f28ae0b..c998f1e 100644 --- a/public/_redirects +++ b/public/_redirects @@ -1,2 +1,7 @@ /* /index.html 200 + X-Frame-Options: DENY + X-Content-Type-Options: nosniff + X-XSS-Protection: 1; mode=block + Referrer-Policy: strict-origin-when-cross-origin + Permissions-Policy: geolocation=(), microphone=(), camera=() diff --git a/src/App.tsx b/src/App.tsx index 788d6d3..12cd6e5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import { Toaster } from "@/components/ui/toaster"; import { Toaster as Sonner } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import ErrorBoundary from "@/components/ErrorBoundary"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import Index from "./pages/Index"; import InteractiveIndex from "./components/InteractiveIndex"; @@ -37,11 +38,12 @@ import NotFound from "./pages/NotFound"; const queryClient = new QueryClient(); const App = () => ( - - - - - + + + + + + } /> } /> @@ -80,9 +82,10 @@ const App = () => ( {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} } /> - - - + + + + ); export default App; diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..d35fa41 --- /dev/null +++ b/src/components/ErrorBoundary.tsx @@ -0,0 +1,76 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; +import { AlertTriangle, RotateCcw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +interface Props { + children: ReactNode; + fallback?: ReactNode; +} + +interface State { + hasError: boolean; + error?: Error; +} + +class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): State { + // Update state so the next render will show the fallback UI + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + // Log error to console in development + if (process.env.NODE_ENV === 'development') { + console.error('Error caught by boundary:', error, errorInfo); + } + } + + handleReset = () => { + this.setState({ hasError: false, error: undefined }); + }; + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback; + } + + return ( +
+ + +
+ +
+ Something went wrong +
+ +

+ We encountered an unexpected error. Please try refreshing the page or contact support if the problem persists. +

+
+ + +
+
+
+
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; \ No newline at end of file diff --git a/src/components/SocialShare.tsx b/src/components/SocialShare.tsx index f48e6ff..a559e0a 100644 --- a/src/components/SocialShare.tsx +++ b/src/components/SocialShare.tsx @@ -5,6 +5,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from import { Share2, Copy, QrCode, Twitter, Facebook, Linkedin } from 'lucide-react'; import { useToast } from '@/hooks/use-toast'; import QRCode from 'qrcode'; +import { validateShareData, rateLimiter } from '@/utils/security'; interface SocialShareProps { title: string; @@ -21,20 +22,38 @@ const SocialShare: React.FC = ({ const [isOpen, setIsOpen] = useState(false); const { toast } = useToast(); + // Validate and sanitize share data + const shareData = validateShareData(title, description, url); + useEffect(() => { if (isOpen) { - QRCode.toDataURL(url, { + QRCode.toDataURL(shareData.url, { width: 200, margin: 2, color: { dark: '#000000', light: '#FFFFFF' } - }).then(setQrCodeDataUrl); + }).then(setQrCodeDataUrl).catch(() => { + toast({ + title: "Error", + description: "Failed to generate QR code", + variant: "destructive", + }); + }); } - }, [url, isOpen]); + }, [shareData.url, isOpen, toast]); const copyToClipboard = async (text: string) => { + if (!rateLimiter.isAllowed('clipboard', 10, 60000)) { + toast({ + title: "Too many attempts", + description: "Please wait before copying again", + variant: "destructive", + }); + return; + } + try { await navigator.clipboard.writeText(text); toast({ @@ -51,6 +70,15 @@ const SocialShare: React.FC = ({ }; const copyQRCode = async () => { + if (!rateLimiter.isAllowed('qr-copy', 5, 60000)) { + toast({ + title: "Too many attempts", + description: "Please wait before copying again", + variant: "destructive", + }); + return; + } + try { const response = await fetch(qrCodeDataUrl); const blob = await response.blob(); @@ -71,9 +99,22 @@ const SocialShare: React.FC = ({ }; const shareLinks = { - twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(title)}&url=${encodeURIComponent(url)}`, - facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`, - linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}` + twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareData.title)}&url=${encodeURIComponent(shareData.url)}`, + facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareData.url)}`, + linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareData.url)}` + }; + + const handleSocialShare = (platform: string, url: string) => { + if (!rateLimiter.isAllowed('social-share', 10, 60000)) { + toast({ + title: "Too many attempts", + description: "Please wait before sharing again", + variant: "destructive", + }); + return; + } + + window.open(url, '_blank', 'noopener,noreferrer'); }; return ( @@ -89,7 +130,7 @@ const SocialShare: React.FC = ({