Agent query: Could you check if the newsletter tiles are loading properly now, without any AWS WAF integration code?
Improve image handling and add security headers. Addresses image loading errors and enhances security by implementing Content-Security-Policy, X-Content-Type-Options, and X-XSS-Protection headers. Screenshot: https://storage.googleapis.com/screenshot-production-us-central1/9dda30b6-4149-4bce-89dc-76333005952c/8166d521-ab6d-49dc-bd03-63e0c10a9767.jpg
This commit is contained in:
@@ -11,11 +11,10 @@ import {
|
||||
Calendar,
|
||||
RefreshCw,
|
||||
Share2,
|
||||
Twitter,
|
||||
Facebook,
|
||||
Rss,
|
||||
Bell,
|
||||
BellOff
|
||||
BellOff,
|
||||
Rss,
|
||||
ImageOff
|
||||
} from "lucide-react";
|
||||
import { useNewsletters, useNewsletterSearch } from "@/lib/newsletter-data";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
@@ -25,6 +24,34 @@ import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
const ITEMS_PER_PAGE = 20;
|
||||
|
||||
function NewsletterImage({ src, alt, onError }: { src: string | null, alt: string, onError: () => void }) {
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const handleError = () => {
|
||||
setError(true);
|
||||
onError();
|
||||
};
|
||||
|
||||
if (!src || error) {
|
||||
return (
|
||||
<div className="w-full h-40 bg-muted flex items-center justify-center rounded-md mb-4">
|
||||
<ImageOff className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="w-full h-40 object-cover rounded-md mb-4"
|
||||
onError={handleError}
|
||||
loading="lazy"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
@@ -236,66 +263,61 @@ export default function Home() {
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
layout
|
||||
>
|
||||
<a
|
||||
href={newsletter.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block"
|
||||
>
|
||||
<Card className="h-full hover:shadow-lg transition-all duration-300 cursor-pointer group">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between gap-2">
|
||||
<span className="line-clamp-2 flex-1">{newsletter.title}</span>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleShare(newsletter);
|
||||
}}
|
||||
>
|
||||
<Share2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.open(newsletter.url, '_blank');
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<CardDescription className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
{format(new Date(newsletter.date), 'MMMM d, yyyy')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
{(newsletter.thumbnail || newsletter.description) && (
|
||||
<CardContent>
|
||||
{newsletter.thumbnail && (
|
||||
<img
|
||||
src={newsletter.thumbnail}
|
||||
alt={newsletter.title}
|
||||
className="w-full h-40 object-cover rounded-md mb-4"
|
||||
/>
|
||||
)}
|
||||
{newsletter.description && (
|
||||
<p className="text-muted-foreground line-clamp-3">
|
||||
{newsletter.description}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
</a>
|
||||
<Card className="h-full hover:shadow-lg transition-all duration-300 cursor-pointer group">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between gap-2">
|
||||
<span className="line-clamp-2">{newsletter.title}</span>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleShare(newsletter);
|
||||
}}
|
||||
>
|
||||
<Share2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.open(newsletter.url, '_blank');
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<CardDescription className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
{format(new Date(newsletter.date), 'MMMM d, yyyy')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
{(newsletter.thumbnail || newsletter.description) && (
|
||||
<CardContent>
|
||||
{newsletter.thumbnail && (
|
||||
<NewsletterImage
|
||||
src={newsletter.thumbnail}
|
||||
alt={newsletter.title}
|
||||
onError={() => {
|
||||
console.warn(`Failed to load image for newsletter: ${newsletter.id}`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{newsletter.description && (
|
||||
<p className="text-muted-foreground line-clamp-3">
|
||||
{newsletter.description}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
</motion.div>
|
||||
))
|
||||
) : (
|
||||
|
||||
@@ -6,6 +6,17 @@ const app = express();
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
|
||||
// Add security headers to prevent script injection
|
||||
app.use((req, res, next) => {
|
||||
// Prevent content injection
|
||||
res.setHeader('Content-Security-Policy', "default-src 'self'; img-src 'self' https: data:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';");
|
||||
// Prevent browsers from MIME-sniffing
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
// XSS protection
|
||||
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||||
next();
|
||||
});
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const start = Date.now();
|
||||
const path = req.path;
|
||||
@@ -47,19 +58,14 @@ app.use((req, res, next) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// importantly only setup vite in development and after
|
||||
// setting up all the other routes so the catch-all route
|
||||
// doesn't interfere with the other routes
|
||||
if (app.get("env") === "development") {
|
||||
await setupVite(app, server);
|
||||
} else {
|
||||
serveStatic(app);
|
||||
}
|
||||
|
||||
// ALWAYS serve the app on port 5000
|
||||
// this serves both the API and the client
|
||||
const PORT = 5000;
|
||||
server.listen(PORT, "0.0.0.0", () => {
|
||||
log(`serving on port ${PORT}`);
|
||||
});
|
||||
})();
|
||||
})();
|
||||
Reference in New Issue
Block a user