diff --git a/frontend/app/api/analytics/events/route.ts b/frontend/app/api/analytics/events/route.ts index 01517a3c..3eb2c857 100644 --- a/frontend/app/api/analytics/events/route.ts +++ b/frontend/app/api/analytics/events/route.ts @@ -26,7 +26,9 @@ export async function POST(req: NextRequest) { try { const body = await req.json(); - const auth = await buildBackendRequestHeaders(req); + const auth = await buildBackendRequestHeaders(req, { + includeSupabaseIdentity: false, + }); const headers = new Headers(auth.headers); headers.set("Content-Type", "application/json"); const res = await fetch(`${API_BASE}/api/analytics/events`, { diff --git a/frontend/app/api/cities/route.ts b/frontend/app/api/cities/route.ts index 8a9d2267..7f1f197b 100644 --- a/frontend/app/api/cities/route.ts +++ b/frontend/app/api/cities/route.ts @@ -10,7 +10,6 @@ import { import { buildCachedJsonResponse } from "@/lib/http-cache"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; -export const dynamic = "force-dynamic"; export async function GET(req: NextRequest) { if (!API_BASE) { @@ -27,7 +26,7 @@ export async function GET(req: NextRequest) { }); const res = await fetch(`${API_BASE}/api/cities`, { headers: auth.headers, - cache: "no-store", + next: { revalidate: 60 }, }); if (!res.ok) { const raw = await res.text(); @@ -38,7 +37,7 @@ export async function GET(req: NextRequest) { const response = buildCachedJsonResponse( req, data, - "no-store, max-age=0", + "public, max-age=0, s-maxage=60, stale-while-revalidate=300", ); return applyAuthResponseCookies(response, auth.response); } catch (error) { diff --git a/frontend/app/api/city/[name]/detail/route.ts b/frontend/app/api/city/[name]/detail/route.ts index 18c9c6cf..b62b2cad 100644 --- a/frontend/app/api/city/[name]/detail/route.ts +++ b/frontend/app/api/city/[name]/detail/route.ts @@ -7,6 +7,7 @@ import { buildProxyExceptionResponse, buildUpstreamErrorResponse, } from "@/lib/api-proxy"; +import { buildCachedJsonResponse } from "@/lib/http-cache"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -42,10 +43,12 @@ export async function GET( const url = `${API_BASE}/api/city/${encodeURIComponent(name)}/detail?${searchParams.toString()}`; try { - const auth = await buildBackendRequestHeaders(req); + const auth = await buildBackendRequestHeaders(req, { + includeSupabaseIdentity: false, + }); const res = await fetch(url, { headers: auth.headers, - cache: "no-store", + next: { revalidate: 15 }, }); if (!res.ok) { const raw = await res.text(); @@ -53,7 +56,11 @@ export async function GET( return applyAuthResponseCookies(response, auth.response); } const data = await res.json(); - const response = NextResponse.json(data); + const response = buildCachedJsonResponse( + req, + data, + "public, max-age=0, s-maxage=15, stale-while-revalidate=45", + ); return applyAuthResponseCookies(response, auth.response); } catch (error) { const response = buildProxyExceptionResponse(error, { diff --git a/frontend/app/api/city/[name]/market-scan/route.ts b/frontend/app/api/city/[name]/market-scan/route.ts index 78b96827..83a0c7f3 100644 --- a/frontend/app/api/city/[name]/market-scan/route.ts +++ b/frontend/app/api/city/[name]/market-scan/route.ts @@ -7,6 +7,7 @@ import { buildProxyExceptionResponse, buildUpstreamErrorResponse, } from "@/lib/api-proxy"; +import { buildCachedJsonResponse } from "@/lib/http-cache"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -45,10 +46,12 @@ export async function GET( const url = `${API_BASE}/api/city/${encodeURIComponent(name)}/market-scan?${params.toString()}`; try { - const auth = await buildBackendRequestHeaders(req); + const auth = await buildBackendRequestHeaders(req, { + includeSupabaseIdentity: false, + }); const res = await fetch(url, { headers: auth.headers, - cache: "no-store", + next: { revalidate: 20 }, }); if (!res.ok) { const raw = await res.text(); @@ -59,11 +62,11 @@ export async function GET( return applyAuthResponseCookies(response, auth.response); } const data = await res.json(); - const response = NextResponse.json(data, { - headers: { - "Cache-Control": "no-store", - }, - }); + const response = buildCachedJsonResponse( + req, + data, + "public, max-age=0, s-maxage=20, stale-while-revalidate=60", + ); return applyAuthResponseCookies(response, auth.response); } catch (error) { const response = buildProxyExceptionResponse(error, { diff --git a/frontend/app/api/city/[name]/route.ts b/frontend/app/api/city/[name]/route.ts index 44b5c469..205d392b 100644 --- a/frontend/app/api/city/[name]/route.ts +++ b/frontend/app/api/city/[name]/route.ts @@ -7,6 +7,7 @@ import { buildProxyExceptionResponse, buildUpstreamErrorResponse, } from "@/lib/api-proxy"; +import { buildCachedJsonResponse } from "@/lib/http-cache"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -131,26 +132,28 @@ export async function GET( const url = `${API_BASE}/api/city/${encodeURIComponent(name)}?force_refresh=${forceRefresh}&depth=${encodeURIComponent(depth)}`; try { - const auth = await buildBackendRequestHeaders(req); + const auth = await buildBackendRequestHeaders(req, { + includeSupabaseIdentity: false, + }); const res = await fetch(url, { headers: auth.headers, - cache: "no-store", + next: { revalidate: 15 }, }); if (!res.ok) { const raw = await res.text(); const summaryUrl = `${API_BASE}/api/city/${encodeURIComponent(name)}/summary?force_refresh=${forceRefresh}`; const summaryRes = await fetch(summaryUrl, { headers: auth.headers, - cache: "no-store", + next: { revalidate: 10 }, }); if (summaryRes.ok) { const summaryData = await summaryRes.json(); - const response = NextResponse.json(buildFallbackCityDetail(name, depth, summaryData), { - headers: { - "Cache-Control": "no-store", - "X-PolyWeather-Fallback": "summary", - }, - }); + const response = buildCachedJsonResponse( + req, + buildFallbackCityDetail(name, depth, summaryData), + "public, max-age=0, s-maxage=10, stale-while-revalidate=30", + ); + response.headers.set("X-PolyWeather-Fallback", "summary"); return applyAuthResponseCookies(response, auth.response); } @@ -158,7 +161,11 @@ export async function GET( return applyAuthResponseCookies(response, auth.response); } const data = normalizeCityDetailPayload(await res.json()); - const response = NextResponse.json(data); + const response = buildCachedJsonResponse( + req, + data, + "public, max-age=0, s-maxage=15, stale-while-revalidate=45", + ); return applyAuthResponseCookies(response, auth.response); } catch (error) { const response = buildProxyExceptionResponse(error, { diff --git a/frontend/app/api/history/[name]/route.ts b/frontend/app/api/history/[name]/route.ts index e2ad2e49..e5d62ca5 100644 --- a/frontend/app/api/history/[name]/route.ts +++ b/frontend/app/api/history/[name]/route.ts @@ -27,7 +27,9 @@ export async function GET( const url = `${API_BASE}/api/history/${encodeURIComponent(name)}`; try { - const auth = await buildBackendRequestHeaders(req); + const auth = await buildBackendRequestHeaders(req, { + includeSupabaseIdentity: false, + }); const fetchOptions = { headers: auth.headers, next: { revalidate: 60 }, diff --git a/frontend/app/api/payments/config/route.ts b/frontend/app/api/payments/config/route.ts index 1ed6e9ae..d2c917be 100644 --- a/frontend/app/api/payments/config/route.ts +++ b/frontend/app/api/payments/config/route.ts @@ -7,6 +7,7 @@ import { buildProxyExceptionResponse, buildUpstreamErrorResponse, } from "@/lib/api-proxy"; +import { buildCachedJsonResponse } from "@/lib/http-cache"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -21,7 +22,7 @@ export async function GET(req: NextRequest) { const auth = await buildBackendRequestHeaders(req); const res = await fetch(`${API_BASE}/api/payments/config`, { headers: auth.headers, - cache: "no-store", + next: { revalidate: 300 }, }); if (!res.ok) { const raw = await res.text(); @@ -31,9 +32,11 @@ export async function GET(req: NextRequest) { return applyAuthResponseCookies(response, auth.response); } const data = await res.json(); - const response = NextResponse.json(data, { - headers: { "Cache-Control": "no-store" }, - }); + const response = buildCachedJsonResponse( + req, + data, + "public, max-age=0, s-maxage=300, stale-while-revalidate=900", + ); return applyAuthResponseCookies(response, auth.response); } catch (error) { return buildProxyExceptionResponse(error, { diff --git a/frontend/app/api/scan/terminal/route.ts b/frontend/app/api/scan/terminal/route.ts index f9f6f12c..ed239a25 100644 --- a/frontend/app/api/scan/terminal/route.ts +++ b/frontend/app/api/scan/terminal/route.ts @@ -7,13 +7,13 @@ import { buildProxyExceptionResponse, buildUpstreamErrorResponse, } from "@/lib/api-proxy"; +import { buildCachedJsonResponse } from "@/lib/http-cache"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; const SCAN_TERMINAL_PROXY_TIMEOUT_MS = Number( process.env.POLYWEATHER_SCAN_TERMINAL_PROXY_TIMEOUT_MS || "28000", ); -export const dynamic = "force-dynamic"; export const maxDuration = 30; export async function GET(req: NextRequest) { @@ -50,10 +50,12 @@ export async function GET(req: NextRequest) { const timeoutId = setTimeout(() => controller.abort(), SCAN_TERMINAL_PROXY_TIMEOUT_MS); try { - auth = await buildBackendRequestHeaders(req); + auth = await buildBackendRequestHeaders(req, { + includeSupabaseIdentity: false, + }); const res = await fetch(url, { headers: auth.headers, - cache: "no-store", + next: { revalidate: 10 }, signal: controller.signal, }); if (!res.ok) { @@ -62,11 +64,11 @@ export async function GET(req: NextRequest) { return applyAuthResponseCookies(response, auth.response); } const data = await res.json(); - const response = NextResponse.json(data, { - headers: { - "Cache-Control": "no-store", - }, - }); + const response = buildCachedJsonResponse( + req, + data, + "public, max-age=0, s-maxage=10, stale-while-revalidate=30", + ); return applyAuthResponseCookies(response, auth.response); } catch (error) { const timedOut = controller.signal.aborted; diff --git a/frontend/app/api/system/status/route.ts b/frontend/app/api/system/status/route.ts index 70e2a88d..c089d406 100644 --- a/frontend/app/api/system/status/route.ts +++ b/frontend/app/api/system/status/route.ts @@ -7,6 +7,7 @@ import { buildProxyExceptionResponse, buildUpstreamErrorResponse, } from "@/lib/api-proxy"; +import { buildCachedJsonResponse } from "@/lib/http-cache"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -19,10 +20,12 @@ export async function GET(req: NextRequest) { } try { - const auth = await buildBackendRequestHeaders(req); + const auth = await buildBackendRequestHeaders(req, { + includeSupabaseIdentity: false, + }); const res = await fetch(`${API_BASE}/api/system/status`, { headers: auth.headers, - cache: "no-store", + next: { revalidate: 30 }, }); if (!res.ok) { const raw = await res.text(); @@ -33,9 +36,11 @@ export async function GET(req: NextRequest) { } const data = await res.json(); - const response = NextResponse.json(data, { - headers: { "Cache-Control": "no-store" }, - }); + const response = buildCachedJsonResponse( + req, + data, + "public, max-age=0, s-maxage=30, stale-while-revalidate=120", + ); return applyAuthResponseCookies(response, auth.response); } catch (error) { return buildProxyExceptionResponse(error, { diff --git a/frontend/app/subscription-help/page.tsx b/frontend/app/subscription-help/page.tsx index e0248a54..58a8d927 100644 --- a/frontend/app/subscription-help/page.tsx +++ b/frontend/app/subscription-help/page.tsx @@ -1,7 +1,6 @@ import type { Metadata } from "next"; import { SubscriptionHelpClient } from "./SubscriptionHelpClient"; - -export const dynamic = "force-dynamic"; +import { I18nProvider } from "@/hooks/useI18n"; export const metadata: Metadata = { title: "PolyWeather | Subscription Help", @@ -9,5 +8,9 @@ export const metadata: Metadata = { }; export default function SubscriptionHelpPage() { - return ; + return ( + + + + ); } diff --git a/frontend/lib/backend-auth.ts b/frontend/lib/backend-auth.ts index 5436f19f..028bd494 100644 --- a/frontend/lib/backend-auth.ts +++ b/frontend/lib/backend-auth.ts @@ -41,17 +41,21 @@ export async function buildBackendRequestHeaders( const incomingAuth = extractBearerToken(request.headers.get("authorization")); const includeSupabaseIdentity = options?.includeSupabaseIdentity !== false; if (hasSupabaseServerEnv() && includeSupabaseIdentity) { - const passthroughResponse = new NextResponse(null, { status: 200 }); - const supabase = createSupabaseRouteClient(request, passthroughResponse); + const supabase = createSupabaseRouteClient(request, new NextResponse(null, { status: 200 })); const { data: { session }, } = await supabase.auth.getSession(); - const { - data: { user }, - } = await supabase.auth.getUser(); + let user = session?.user ?? null; + if (session) { + const { + data: { user: validated }, + } = await supabase.auth.getUser(); + user = validated ?? session.user; + } - const forwardedUserId = String(user?.id || session?.user?.id || "").trim(); - const forwardedEmail = String(user?.email || session?.user?.email || "").trim(); + const passthroughResponse = new NextResponse(null, { status: 200 }); + const forwardedUserId = String(user?.id || "").trim(); + const forwardedEmail = String(user?.email || "").trim(); if (forwardedUserId) { headers.set(FORWARDED_SUPABASE_USER_ID_HEADER, forwardedUserId); } diff --git a/frontend/middleware.ts b/frontend/middleware.ts index 6ca128e0..e6545e33 100644 --- a/frontend/middleware.ts +++ b/frontend/middleware.ts @@ -20,23 +20,6 @@ const SUPABASE_AUTH_REQUIRED = readEnvBool( SUPABASE_AUTH_ENABLED, ); -function isStaticAsset(pathname: string) { - return ( - pathname.startsWith("/_next/") || - pathname.startsWith("/favicon") || - pathname === "/apple-touch-icon.png" || - pathname === "/manifest.webmanifest" || - pathname === "/site.webmanifest" || - pathname.startsWith("/android-chrome-") || - pathname.startsWith("/robots.txt") || - pathname.startsWith("/sitemap.xml") || - pathname.startsWith("/icons/") || - pathname.startsWith("/images/") || - pathname.startsWith("/scenery/") || - pathname.startsWith("/static/") - ); -} - function isPublicPage(pathname: string) { return ( pathname === "/" || @@ -78,7 +61,7 @@ function handleLegacyTokenGate(request: NextRequest) { } const { pathname, searchParams } = request.nextUrl; - if (isStaticAsset(pathname) || isPublicPage(pathname) || isPublicApi(pathname)) { + if (isPublicPage(pathname) || isPublicApi(pathname)) { return NextResponse.next(); } @@ -178,10 +161,6 @@ async function handleSupabaseOptionalSession(request: NextRequest) { } export async function middleware(request: NextRequest) { - const { pathname } = request.nextUrl; - if (isStaticAsset(pathname)) { - return NextResponse.next(); - } const requestHost = request.headers.get("x-forwarded-host") || request.headers.get("host") || @@ -203,5 +182,15 @@ export async function middleware(request: NextRequest) { } export const config = { - matcher: ["/((?!_next/static|_next/image).*)"], + matcher: [ + "/account/:path*", + "/ops/:path*", + "/api/auth/:path*", + "/api/ops/:path*", + "/api/payments/:path*", + "/api/system/:path*", + "/api/history/:path*", + "/api/city/:path*/detail:path*", + "/api/scan/terminal/ai:path*", + ], }; diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index d5456a15..b8329483 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -1,6 +1,17 @@ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, + async headers() { + const cacheHeader = { + key: "Cache-Control", + value: "public, max-age=31536000, immutable", + }; + const staticExts = ["jpg", "jpeg", "png", "gif", "ico", "svg", "webp", "avif", "woff2", "ttf", "eot", "css", "js"]; + return staticExts.map((ext) => ({ + source: `/:path(.+\\.${ext})`, + headers: [cacheHeader], + })); + }, }; export default nextConfig;