chore(infra): auth fast-path, pg pool tuning, react-doctor devdep
Three coordinated changes that improve CI/build ergonomics and dev-mode performance. None change database schema; all are reversible by env var. src/lib/auth.ts Wrap Neon Auth getSession() with a fast-path that returns null when NEON_AUTH_BASE_URL is unset/placeholder (CI build-time value) OR when the dev_session cookie is set with a recognized role. The real getSession() does a network fetch and pays a 30s DNS timeout when the env var is unset. Admin pages each call getSession (directly or via getAdminUser()) — paying that tax per page load is unacceptable. src/lib/db.ts + db/client.ts Raise PG_POOL_MAX default 10 -> 50 (Vercel function concurrency can exceed 10 with parallel RPCs). Lower PG_POOL_CONN_TIMEOUT_MS default 30s -> 5s in src/lib/db.ts and 10s -> 5s in db/client.ts so a bad Neon branch fails fast instead of hanging requests. src/lib/admin-permissions.ts + src/proxy.ts Add PERF_TEST_AUTH=1 env var that lets dev_session cookies bypass Neon Auth in production mode. Used by Playwright perf benchmarks against authenticated admin pages. NEVER set this in real production — the dev_session cookie then grants platform_admin / brand_admin / store_employee with no password check. The middleware guard in proxy.ts and getAdminUser() both honor the flag; the flag is the only toggle needed. package.json Add react-doctor ^0.5.8 to devDependencies (used to drive the lint cleanup that landed in the 38 commits already on this branch).
This commit is contained in:
+2
-2
@@ -74,10 +74,10 @@ function getPool(): Pool {
|
|||||||
}
|
}
|
||||||
_pool = new Pool({
|
_pool = new Pool({
|
||||||
connectionString,
|
connectionString,
|
||||||
max: parseInt(process.env.PG_POOL_MAX ?? "10", 10),
|
max: parseInt(process.env.PG_POOL_MAX ?? "50", 10),
|
||||||
idleTimeoutMillis: parseInt(process.env.PG_POOL_IDLE_MS ?? "30000", 10),
|
idleTimeoutMillis: parseInt(process.env.PG_POOL_IDLE_MS ?? "30000", 10),
|
||||||
connectionTimeoutMillis: parseInt(
|
connectionTimeoutMillis: parseInt(
|
||||||
process.env.PG_POOL_CONN_TIMEOUT_MS ?? "10000",
|
process.env.PG_POOL_CONN_TIMEOUT_MS ?? "5000",
|
||||||
10,
|
10,
|
||||||
),
|
),
|
||||||
allowExitOnIdle: false,
|
allowExitOnIdle: false,
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"jsdom": "^25.0.1",
|
"jsdom": "^25.0.1",
|
||||||
"pg": "^8.20.0",
|
"pg": "^8.20.0",
|
||||||
"playwright": "^1.59.1",
|
"playwright": "^1.59.1",
|
||||||
|
"react-doctor": "^0.5.8",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"tsx": "^4.22.4",
|
"tsx": "^4.22.4",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ import type { AdminRole, AdminUser, TenantContext } from "@/lib/admin-permission
|
|||||||
* shows "Access Denied" — correct behavior.
|
* shows "Access Denied" — correct behavior.
|
||||||
*/
|
*/
|
||||||
export async function getAdminUser(): Promise<AdminUser | null> {
|
export async function getAdminUser(): Promise<AdminUser | null> {
|
||||||
// Check for dev_session cookie in development mode
|
// Check for dev_session cookie in development mode, or when
|
||||||
if (process.env.NODE_ENV !== "production") {
|
// PERF_TEST_AUTH=1 (for production-mode perf benchmarks against
|
||||||
|
// authenticated admin pages — never enable in real production).
|
||||||
|
if (process.env.NODE_ENV !== "production" || process.env.PERF_TEST_AUTH === "1") {
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
const devSession = cookieStore.get("dev_session")?.value;
|
const devSession = cookieStore.get("dev_session")?.value;
|
||||||
if (devSession === "platform_admin" || devSession === "brand_admin" || devSession === "store_employee") {
|
if (devSession === "platform_admin" || devSession === "brand_admin" || devSession === "store_employee") {
|
||||||
|
|||||||
+55
-2
@@ -19,14 +19,67 @@ import "server-only";
|
|||||||
|
|
||||||
import { createNeonAuth } from "@neondatabase/auth/next/server";
|
import { createNeonAuth } from "@neondatabase/auth/next/server";
|
||||||
import { getNeonAuthConfig } from "@/auth.config";
|
import { getNeonAuthConfig } from "@/auth.config";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
const auth = createNeonAuth({
|
const auth = createNeonAuth({
|
||||||
baseUrl: getNeonAuthConfig().baseUrl,
|
baseUrl: getNeonAuthConfig().baseUrl,
|
||||||
cookies: { secret: getNeonAuthConfig().cookieSecret },
|
cookies: { secret: getNeonAuthConfig().cookieSecret },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { getSession, signIn, signOut, resetPassword, requestPasswordReset } = auth;
|
const { getSession: _realGetSession, signIn, signOut, resetPassword, requestPasswordReset } = auth;
|
||||||
export const { listUsers, createUser, setRole, setUserPassword, updateUser } = auth.admin;
|
const { listUsers, createUser, setRole, setUserPassword, updateUser } = auth.admin;
|
||||||
|
|
||||||
|
export { signIn, signOut, resetPassword, requestPasswordReset };
|
||||||
|
export { listUsers, createUser, setRole, setUserPassword, updateUser };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fast-path wrapper around Neon Auth's `getSession()`.
|
||||||
|
*
|
||||||
|
* Neon Auth's real `getSession()` does a network fetch to the configured
|
||||||
|
* Auth base URL. When the env var is unset (or set to the CI build-time
|
||||||
|
* `placeholder.local`), every call hits a 30s DNS timeout before
|
||||||
|
* returning `null`. Admin pages each call `getSession()` (directly or
|
||||||
|
* via `getAdminUser()`) — paying that 30s tax on every page load is
|
||||||
|
* unacceptable.
|
||||||
|
*
|
||||||
|
* Short-circuits when:
|
||||||
|
* 1. `NEON_AUTH_BASE_URL` is unset / placeholder — Neon Auth can't
|
||||||
|
* resolve anything useful; return `null` immediately.
|
||||||
|
* 2. The `dev_session` cookie is set with a recognized role — we
|
||||||
|
* trust it as a development bypass and skip the network fetch.
|
||||||
|
*
|
||||||
|
* In production with a real `NEON_AUTH_BASE_URL` and no dev_session,
|
||||||
|
* falls through to the real Neon Auth lookup unchanged.
|
||||||
|
*/
|
||||||
|
function isAuthConfigured(): boolean {
|
||||||
|
const url = process.env.NEON_AUTH_BASE_URL;
|
||||||
|
if (!url || url === "placeholder" || url === "https://placeholder.local") return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isDevSession(): Promise<boolean> {
|
||||||
|
if (process.env.NODE_ENV === "production" && process.env.PERF_TEST_AUTH !== "1") return false;
|
||||||
|
try {
|
||||||
|
const store = await cookies();
|
||||||
|
const v = store.get("dev_session")?.value;
|
||||||
|
return v === "platform_admin" || v === "brand_admin" || v === "store_employee";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSession(): Promise<Awaited<ReturnType<typeof _realGetSession>>> {
|
||||||
|
if (!isAuthConfigured()) {
|
||||||
|
if (process.env.PERF_TEST_AUTH === "1") console.log("[auth] getSession short-circuit: not configured");
|
||||||
|
return { data: null, error: null } as Awaited<ReturnType<typeof _realGetSession>>;
|
||||||
|
}
|
||||||
|
if (await isDevSession()) {
|
||||||
|
if (process.env.PERF_TEST_AUTH === "1") console.log("[auth] getSession short-circuit: dev_session");
|
||||||
|
return { data: null, error: null } as Awaited<ReturnType<typeof _realGetSession>>;
|
||||||
|
}
|
||||||
|
if (process.env.PERF_TEST_AUTH === "1") console.log("[auth] getSession calling real Neon Auth");
|
||||||
|
return _realGetSession();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Re-exported for the API route handler compatibility.
|
* Re-exported for the API route handler compatibility.
|
||||||
|
|||||||
+2
-2
@@ -48,10 +48,10 @@ function buildPool(): Pool {
|
|||||||
// PG_POOL_MAX (default 10)
|
// PG_POOL_MAX (default 10)
|
||||||
// PG_POOL_IDLE_MS (default 30s)
|
// PG_POOL_IDLE_MS (default 30s)
|
||||||
// PG_POOL_CONN_TIMEOUT_MS (default 10s)
|
// PG_POOL_CONN_TIMEOUT_MS (default 10s)
|
||||||
max: parseInt(process.env.PG_POOL_MAX ?? "10", 10),
|
max: parseInt(process.env.PG_POOL_MAX ?? "50", 10),
|
||||||
idleTimeoutMillis: parseInt(process.env.PG_POOL_IDLE_MS ?? "30000", 10),
|
idleTimeoutMillis: parseInt(process.env.PG_POOL_IDLE_MS ?? "30000", 10),
|
||||||
connectionTimeoutMillis: parseInt(
|
connectionTimeoutMillis: parseInt(
|
||||||
process.env.PG_POOL_CONN_TIMEOUT_MS ?? "30000",
|
process.env.PG_POOL_CONN_TIMEOUT_MS ?? "5000",
|
||||||
10,
|
10,
|
||||||
),
|
),
|
||||||
// Vercel/serverless recycling: keep the pool hot for warm invocations.
|
// Vercel/serverless recycling: keep the pool hot for warm invocations.
|
||||||
|
|||||||
+5
-2
@@ -104,8 +104,11 @@ function hasNeonAuthSession(request: NextRequest): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Development bypass: allow dev_session cookie for local testing
|
// Development bypass: allow dev_session cookie for local testing
|
||||||
// This bypass only works when NODE_ENV is not "production"
|
// This bypass works when NODE_ENV is not "production", OR when
|
||||||
if (process.env.NODE_ENV !== "production") {
|
// PERF_TEST_AUTH=1 is set (for production-mode perf benchmarks against
|
||||||
|
// authenticated admin pages — never enable in real production).
|
||||||
|
const perfTestAuth = process.env.PERF_TEST_AUTH === "1";
|
||||||
|
if (process.env.NODE_ENV !== "production" || perfTestAuth) {
|
||||||
const devSession = cookies.get("dev_session")?.value;
|
const devSession = cookies.get("dev_session")?.value;
|
||||||
if (devSession === "platform_admin" || devSession === "brand_admin" || devSession === "store_employee") {
|
if (devSession === "platform_admin" || devSession === "brand_admin" || devSession === "store_employee") {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
Reference in New Issue
Block a user