infoalloggi-monorepo/apps/infoalloggi/src/middlewares/api_middleware.ts

207 lines
5.8 KiB
TypeScript
Raw Normal View History

import { type NextRequest, NextResponse } from "next/server";
import { env } from "~/env";
import { TOKEN_CONFIG } from "~/server/auth/configs";
import { verifyToken } from "~/server/auth/jwt";
export const apisMiddleware = async (req: NextRequest) => {
const { pathname, searchParams } = req.nextUrl;
// Only handle storage API routes
if (!pathname.startsWith("/storage-api/")) {
return null; // Pass to next middleware
}
const params = new URLSearchParams(searchParams);
const isImageRequest = params.get("media") === "image";
const isVideoRequest = params.get("media") === "video";
// Check if this is a Next.js Image Optimization request
const purpose = req.headers.get("purpose");
const isNextImageRequest =
purpose === "prefetch" || req.headers.get("x-vercel-id");
// For regular requests (not from Next Image Optimization), check auth
if (!isNextImageRequest && !isImageRequest && !isVideoRequest) {
const accessToken = req.cookies.get(
TOKEN_CONFIG.ACCESS_TOKEN_COOKIE_NAME,
)?.value;
if (!accessToken) {
console.log(
"No access token found, referrer:",
req.referrer,
" url:",
req.url,
);
return new NextResponse("Unauthorized", { status: 401 });
}
const payload = await verifyToken(accessToken);
if (!payload) {
console.log("Invalid token, referrer:", req.referrer, " url:", req.url);
return new NextResponse("Unauthorized", { status: 401 });
}
}
// Build the storage API URL correctly
const slug = pathname.replace("/storage-api/", "");
params.set("token", env.STORAGE_TOKEN);
const storageUrl = `${env.STORAGE_URL}/${slug}?${params.toString()}`;
try {
// Forward the request to storage API
const headers = new Headers(req.headers);
headers.delete("host");
headers.delete("cookie");
// **CRITICAL: Preserve Range header for video streaming**
const rangeHeader = req.headers.get("range");
const response = await fetch(storageUrl, {
method: req.method,
headers: headers,
body:
req.method !== "GET" && req.method !== "HEAD" ? req.body : undefined,
// @ts-expect-error - duplex needed for request streaming
duplex: "half",
});
// Handle failed responses
if (!response.ok) {
console.error(
`Storage API error: ${response.status} for ${storageUrl}, referrer: ${req.referrer}, url: ${req.url}`,
);
// Return fallback for image requests
if (isImageRequest || isNextImageRequest) {
const url = req.nextUrl.clone();
url.pathname = "/fallback-image.png";
url.search = "";
return NextResponse.rewrite(url);
}
// Return fallback for video requests
if (isVideoRequest) {
const url = req.nextUrl.clone();
url.pathname = "/fallback-video.png";
url.search = "";
return NextResponse.rewrite(url);
}
return new NextResponse("Storage API error", { status: response.status });
}
// Get response data
const data = await response.arrayBuffer();
// Check if we got valid data
if (!data || data.byteLength === 0) {
console.error(
"Received empty response from storage API, referrer:",
req.referrer,
" url:",
req.url,
);
if (isImageRequest || isNextImageRequest) {
const url = req.nextUrl.clone();
url.pathname = "/fallback-image.png";
url.search = "";
return NextResponse.rewrite(url);
}
if (isVideoRequest) {
const url = req.nextUrl.clone();
url.pathname = "/fallback-video.png";
url.search = "";
return NextResponse.rewrite(url);
}
return new NextResponse("Empty response", { status: 500 });
}
// **IMPORTANT: Use correct status code for partial content**
const status =
rangeHeader && response.status === 206 ? 206 : response.status;
// Create response with proper headers
const proxyResponse = new NextResponse(data, {
status: status,
statusText: response.statusText,
});
// Copy relevant headers
const contentType = response.headers.get("Content-Type");
const contentDisposition = response.headers.get("Content-Disposition");
const contentLength = response.headers.get("Content-Length");
const acceptRanges = response.headers.get("Accept-Ranges");
const contentRange = response.headers.get("Content-Range");
if (contentType) {
proxyResponse.headers.set("Content-Type", contentType);
}
if (contentDisposition) {
proxyResponse.headers.set("Content-Disposition", contentDisposition);
}
if (contentLength) {
proxyResponse.headers.set("Content-Length", contentLength);
}
// **CRITICAL: Copy Range-related headers for video streaming**
if (acceptRanges) {
proxyResponse.headers.set("Accept-Ranges", acceptRanges);
} else if (isVideoRequest) {
proxyResponse.headers.set("Accept-Ranges", "bytes");
}
if (contentRange) {
proxyResponse.headers.set("Content-Range", contentRange);
}
// Set CORS and caching headers
proxyResponse.headers.set("Access-Control-Allow-Origin", "*");
// Different cache strategy for videos
if (isVideoRequest) {
proxyResponse.headers.set(
"Cache-Control",
"public, max-age=31536000, immutable",
);
} else {
proxyResponse.headers.set(
"Cache-Control",
"public, max-age=31536000, immutable",
);
}
proxyResponse.headers.delete("X-Frame-Options");
proxyResponse.headers.set(
"Content-Security-Policy",
"frame-ancestors 'self'",
);
return proxyResponse;
} catch (error) {
console.error("Storage proxy error:", error);
// Return fallback on error
if (isImageRequest || isNextImageRequest) {
const url = req.nextUrl.clone();
url.pathname = "/fallback-image.png";
url.search = "";
return NextResponse.rewrite(url);
}
if (isVideoRequest) {
const url = req.nextUrl.clone();
url.pathname = "/fallback-video.png";
url.search = "";
return NextResponse.rewrite(url);
}
return new NextResponse("Failed to proxy request", { status: 500 });
}
};