Reduce Supabase disk IO
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()}` : ""
|
||||
|
||||
Reference in New Issue
Block a user