feat(api): implement rate limiting and SSRF protection across endpoints

- Added rate limiting to `reaction-users`, `search`, and `image-proxy` APIs to prevent abuse.
- Introduced SSRF protection in `image-proxy` to block requests to private IP ranges.
- Enhanced `link-preview` to use `linkedom` for HTML parsing and improved meta tag extraction.
- Refactored authentication checks in various pages to utilize middleware for cleaner code.
- Improved JWT key loading with error handling and security warnings for production.
- Updated `authFetch` utility to handle token refresh more efficiently with deduplication.
- Enhanced rate limiting utility to trust proxy headers from known sources.
- Numerous layout / design changes
This commit is contained in:
2025-12-05 14:21:52 -05:00
parent 55e4c5ff0c
commit e18aa3f42c
44 changed files with 3512 additions and 892 deletions

View File

@@ -17,7 +17,10 @@ const Memes = () => {
const [hasMore, setHasMore] = useState(true);
const [selectedImage, setSelectedImage] = useState(null);
const [selectedIndex, setSelectedIndex] = useState(-1);
const [imageLoading, setImageLoading] = useState({});
const observerRef = useRef();
const touchStartRef = useRef(null);
const touchEndRef = useRef(null);
const theme = document.documentElement.getAttribute("data-theme")
const cacheRef = useRef({ pagesLoaded: new Set(), items: [] });
@@ -158,6 +161,43 @@ const Memes = () => {
setSelectedIndex(-1);
}, []);
// Touch swipe handlers for mobile navigation
const handleTouchStart = useCallback((e) => {
touchStartRef.current = e.touches[0].clientX;
touchEndRef.current = null;
}, []);
const handleTouchMove = useCallback((e) => {
touchEndRef.current = e.touches[0].clientX;
}, []);
const handleTouchEnd = useCallback(() => {
if (!touchStartRef.current || !touchEndRef.current) return;
const distance = touchStartRef.current - touchEndRef.current;
const minSwipeDistance = 50;
if (Math.abs(distance) > minSwipeDistance) {
if (distance > 0) {
// Swiped left -> next
handleNavigate(1);
} else {
// Swiped right -> prev
handleNavigate(-1);
}
}
touchStartRef.current = null;
touchEndRef.current = null;
}, [handleNavigate]);
// Track image loading state
const handleImageLoad = useCallback((id) => {
setImageLoading(prev => ({ ...prev, [id]: false }));
}, []);
const handleImageLoadStart = useCallback((id) => {
setImageLoading(prev => ({ ...prev, [id]: true }));
}, []);
const handleCopyImage = useCallback(async () => {
if (!selectedImage) return;
try {
@@ -181,6 +221,7 @@ const Memes = () => {
<div className="grid-container">
{images.map((img, i) => {
const isLast = i === images.length - 1;
const isLoading = imageLoading[img.id] !== false;
return (
<div
key={img.id}
@@ -191,13 +232,18 @@ const Memes = () => {
setSelectedIndex(i);
prefetchImage(images[i + 1]);
}}
style={{ cursor: 'pointer' }}
style={{ cursor: 'pointer', position: 'relative' }}
>
{isLoading && (
<div className="meme-skeleton" />
)}
<Image
src={img.url}
alt={`meme-${img.id}`}
imageClassName="meme-img"
imageClassName={`meme-img ${isLoading ? 'meme-img-loading' : ''}`}
loading="lazy"
onLoad={() => handleImageLoad(img.id)}
onLoadStart={() => handleImageLoadStart(img.id)}
/>
</div>
);
@@ -239,7 +285,12 @@ const Memes = () => {
dismissableMask={true}
>
{selectedImage && (
<div className="meme-dialog-body">
<div
className="meme-dialog-body"
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
<button
type="button"
className="meme-dialog-nav meme-dialog-nav-prev"