Reduce Supabase disk IO

This commit is contained in:
2569718930@qq.com
2026-05-29 17:22:33 +08:00
parent 9e3a4e2f45
commit 2269aaefd7
63 changed files with 4637 additions and 410 deletions
+33 -27
View File
@@ -6,7 +6,7 @@ export const BACKEND_ENTITLEMENT_HEADER = "x-polyweather-entitlement";
export const FORWARDED_SUPABASE_USER_ID_HEADER = "x-polyweather-auth-user-id";
export const FORWARDED_SUPABASE_EMAIL_HEADER = "x-polyweather-auth-email";
type HeaderBuildResult = {
export type BackendHeaderBuildResult = {
headers: HeadersInit;
response: NextResponse | null;
authUserId?: string | null;
@@ -26,10 +26,22 @@ function extractBearerToken(headerValue: string | null) {
return "";
}
function hasSupabaseSessionCookie(request: NextRequest) {
return request.cookies.getAll().some((cookie) => {
const name = cookie.name.toLowerCase();
const value = String(cookie.value || "").trim();
if (!value) return false;
return (
name === "supabase-auth-token" ||
(name.startsWith("sb-") && name.includes("-auth-token"))
);
});
}
export async function buildBackendRequestHeaders(
request: NextRequest,
options?: HeaderBuildOptions,
): Promise<HeaderBuildResult> {
): Promise<BackendHeaderBuildResult> {
const headers = new Headers({
Accept: "application/json",
});
@@ -39,31 +51,19 @@ export async function buildBackendRequestHeaders(
}
const incomingAuth = extractBearerToken(request.headers.get("authorization"));
if (incomingAuth) {
headers.set("Authorization", `Bearer ${incomingAuth}`);
return { headers, response: null, authUserId: null, authEmail: null };
}
const includeSupabaseIdentity = options?.includeSupabaseIdentity !== false;
if (hasSupabaseServerEnv() && includeSupabaseIdentity) {
if (!hasSupabaseSessionCookie(request)) {
return { headers, response: null, authUserId: null, authEmail: null };
}
const passthroughResponse = new NextResponse(null, { status: 200 });
const supabase = createSupabaseRouteClient(request, passthroughResponse);
// Always call getUser() to ensure token is validated and refreshed if expired
const {
data: { user },
} = await supabase.auth.getUser();
const forwardedUserId = String(user?.id || "").trim();
const forwardedEmail = String(user?.email || "").trim();
if (forwardedUserId) {
headers.set(FORWARDED_SUPABASE_USER_ID_HEADER, forwardedUserId);
}
if (forwardedEmail) {
headers.set(FORWARDED_SUPABASE_EMAIL_HEADER, forwardedEmail);
}
if (incomingAuth) {
headers.set("Authorization", `Bearer ${incomingAuth}`);
return { headers, response: passthroughResponse, authUserId: forwardedUserId || null, authEmail: forwardedEmail || null };
}
// Call getSession() to get the updated access token (after getUser() has refreshed it if needed)
const {
data: { session },
} = await supabase.auth.getSession();
@@ -72,12 +72,18 @@ export async function buildBackendRequestHeaders(
// Fallback to cookie-backed session when request does not carry bearer.
headers.set("Authorization", `Bearer ${accessToken}`);
}
const sessionUser = session?.user;
const forwardedUserId = String(sessionUser?.id || "").trim();
const forwardedEmail = String(sessionUser?.email || "").trim();
if (forwardedUserId) {
headers.set(FORWARDED_SUPABASE_USER_ID_HEADER, forwardedUserId);
}
if (forwardedEmail) {
headers.set(FORWARDED_SUPABASE_EMAIL_HEADER, forwardedEmail);
}
return { headers, response: passthroughResponse, authUserId: forwardedUserId || null, authEmail: forwardedEmail || null };
}
if (incomingAuth) {
headers.set("Authorization", `Bearer ${incomingAuth}`);
}
return { headers, response: null, authUserId: null, authEmail: null };
}
@@ -94,7 +100,7 @@ export function applyAuthResponseCookies(
return target;
}
export function requireBackendAuthUser(auth: HeaderBuildResult) {
export function requireBackendAuthUser(auth: BackendHeaderBuildResult) {
if (auth.authUserId) return null;
return applyAuthResponseCookies(
NextResponse.json(
+18 -8
View File
@@ -1,6 +1,10 @@
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { createSupabaseServerClient, hasSupabaseServerEnv } from "@/lib/supabase/server";
import {
createSupabaseServerClient,
hasSupabaseServerEnv,
hasSupabaseSessionCookieValues,
} from "@/lib/supabase/server";
function parseAdminEmails() {
return String(process.env.POLYWEATHER_OPS_ADMIN_EMAILS || "")
@@ -16,12 +20,17 @@ export async function requireOpsAdmin(nextPath = "/ops") {
}
const cookieStore = await cookies();
const supabaseCookies = cookieStore.getAll().map((item) => ({
name: item.name,
value: item.value,
}));
if (!hasSupabaseSessionCookieValues(supabaseCookies)) {
redirect(`/auth/login?next=${encodeURIComponent(nextPath)}`);
}
const supabase = createSupabaseServerClient({
getAll() {
return cookieStore.getAll().map((item) => ({
name: item.name,
value: item.value,
}));
return supabaseCookies;
},
setAll() {
// Server components cannot persist refreshed cookies. Route handlers keep
@@ -30,10 +39,11 @@ export async function requireOpsAdmin(nextPath = "/ops") {
});
const {
data: { user },
} = await supabase.auth.getUser();
data,
error,
} = await supabase.auth.getClaims();
const email = String(user?.email || "").trim().toLowerCase();
const email = error ? "" : String(data?.claims?.email || "").trim().toLowerCase();
if (!email) {
redirect(`/auth/login?next=${encodeURIComponent(nextPath)}`);
}
+7
View File
@@ -67,6 +67,13 @@ export const opsApi = {
memberships() {
return opsFetch<Record<string, unknown>>("/api/ops/memberships?limit=200");
},
membershipsOverview(limit = 200, days = 90) {
return opsFetch<{
memberships?: Array<Record<string, unknown>>;
days?: number;
daily?: { date: string; trial: number; paid: number; total: number; cumulative: number }[];
}>(`/api/ops/memberships/overview?limit=${limit}&days=${days}`);
},
membershipsGrowth(days = 90) {
return opsFetch<{
days: number;
+27
View File
@@ -0,0 +1,27 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import {
applyAuthResponseCookies,
type BackendHeaderBuildResult,
} from "@/lib/backend-auth";
function hasBearerAuth(request: NextRequest) {
const raw = request.headers.get("authorization");
if (!raw) return false;
const parts = raw.trim().split(/\s+/);
return parts.length === 2 && parts[0].toLowerCase() === "bearer" && Boolean(parts[1]);
}
export function requireOpsProxyAuth(
request: NextRequest,
auth: BackendHeaderBuildResult,
): NextResponse | null {
if (auth.authUserId || hasBearerAuth(request)) return null;
return applyAuthResponseCookies(
NextResponse.json(
{ error: "Unauthorized", detail: "Supabase session required" },
{ status: 401 },
),
auth.response,
);
}
+33 -7
View File
@@ -17,6 +17,20 @@ export function hasSupabaseServerEnv() {
return Boolean(url && anonKey);
}
export function hasSupabaseSessionCookieValues(
cookies: { name: string; value: string }[],
) {
return cookies.some((cookie) => {
const name = cookie.name.toLowerCase();
const value = String(cookie.value || "").trim();
if (!value) return false;
return (
name === "supabase-auth-token" ||
(name.startsWith("sb-") && name.includes("-auth-token"))
);
});
}
export function createSupabaseServerClient(
cookieAdapter: CookieAdapter,
) {
@@ -79,12 +93,17 @@ export async function refreshMiddlewareSession(request: NextRequest) {
return { response, user: null };
}
const supabaseCookies = request.cookies.getAll().map((item) => ({
name: item.name,
value: item.value,
}));
if (!hasSupabaseSessionCookieValues(supabaseCookies)) {
return { response, user: null };
}
const supabase = createSupabaseServerClient({
getAll() {
return request.cookies.getAll().map((item) => ({
name: item.name,
value: item.value,
}));
return supabaseCookies;
},
setAll(cookiesToSet) {
for (const cookie of cookiesToSet) {
@@ -103,11 +122,18 @@ export async function refreshMiddlewareSession(request: NextRequest) {
try {
const {
data: { user },
} = await supabase.auth.getUser();
data,
error,
} = await supabase.auth.getClaims();
if (error || !data?.claims?.sub) {
return { response, user: null };
}
const user = {
id: String(data.claims.sub || ""),
email: String(data.claims.email || ""),
};
return { response, user };
} catch {
return { response, user: null };
}
}
+3 -1
View File
@@ -34,7 +34,9 @@ export async function forwardPriorityWarmHint(req: NextRequest) {
}
try {
const auth = await buildBackendRequestHeaders(req);
const auth = await buildBackendRequestHeaders(req, {
includeSupabaseIdentity: false,
});
const res = await fetch(
`${API_BASE}/api/system/priority-warm${
params.size ? `?${params.toString()}` : ""