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

@@ -1,7 +1,24 @@
// requireAuthHook.js
import { API_URL } from "@/config";
// WeakMap to cache auth promises per Astro context (request)
const authCache = new WeakMap();
export const requireAuthHook = async (Astro) => {
// Check if we already have a cached promise for this request
if (authCache.has(Astro)) {
return authCache.get(Astro);
}
// Create a promise and cache it immediately to prevent race conditions
const authPromise = performAuth(Astro);
authCache.set(Astro, authPromise);
// Return the promise - all callers will await the same promise
return authPromise;
};
async function performAuth(Astro) {
try {
const cookieHeader = Astro.request.headers.get("cookie") ?? "";
let res = await fetch(`${API_URL}/auth/id`, {
@@ -21,19 +38,30 @@ export const requireAuthHook = async (Astro) => {
return null;
}
const setCookieHeader = refreshRes.headers.get("set-cookie");
let newCookieHeader = cookieHeader;
if (setCookieHeader) {
const cookiesArray = setCookieHeader.split(/,(?=\s*\w+=)/);
cookiesArray.forEach((c) => Astro.response.headers.append("set-cookie", c));
newCookieHeader = cookiesArray.map(c => c.split(";")[0]).join("; ");
// Get all Set-Cookie headers (getSetCookie returns an array)
let setCookies = [];
if (typeof refreshRes.headers.getSetCookie === 'function') {
setCookies = refreshRes.headers.getSetCookie();
} else {
// Fallback for older Node versions
const setCookieHeader = refreshRes.headers.get("set-cookie");
if (setCookieHeader) {
// Split on comma followed by a cookie name (word=), avoiding splitting on Expires dates
setCookies = setCookieHeader.split(/,(?=\s*[a-zA-Z_][a-zA-Z0-9_]*=)/);
}
}
if (setCookies.length === 0) {
console.error("No set-cookie header found in refresh response");
return null;
}
// Forward cookies to client
setCookies.forEach((c) => Astro.response.headers.append("set-cookie", c.trim()));
// Build new cookie header for the retry request
const newCookieHeader = setCookies.map(c => c.split(";")[0].trim()).join("; ");
res = await fetch(`${API_URL}/auth/id`, {
headers: { Cookie: newCookieHeader },
credentials: "include",
@@ -52,4 +80,4 @@ export const requireAuthHook = async (Astro) => {
console.error("[SSR] requireAuthHook error:", err);
return null;
}
};
}