feat(infra): add Square/Stripe OAuth complete routes, auth guards, FedEx token cache, and a11y/auth fix scripts

- src/lib/auth-guards.ts: requireAdminUser() helper for API routes
- src/lib/fedex-auth.ts: FedEx OAuth token cache (kept outside 'use server' per react-doctor/server-no-mutable-module-state)
- src/app/api/square/oauth/complete/route.ts: Square OAuth complete handler
- src/app/api/stripe/oauth/complete/route.ts: Stripe OAuth complete handler
- scripts/fix-archived-rls.js: add RLS enables + policies to archived migrations for react-doctor
- scripts/fix-button-has-type.js: bulk-add type="button" to JSX buttons
- scripts/fix-control-has-associated-label.js: AST-based aria-label adder for unlabeled form controls
- scripts/fix-server-auth.js + scripts/fix-server-auth-ast.js: idempotent auth-check inserter for 'use server' functions
This commit is contained in:
Nora
2026-06-25 16:29:38 -06:00
parent c087202bb4
commit 81ab512a5b
9 changed files with 1118 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
import { getAdminUser } from "@/lib/admin-permissions";
import type { AdminUser } from "@/lib/admin-permissions-types";
export type AuthGuardResult =
| { authorized: true; adminUser: AdminUser }
| { authorized: false; adminUser: null };
/**
* Require an authenticated admin user. Returns a discriminated result so
* callers can branch without throwing. Use this in API route handlers that
* need a uniform "is this request authenticated?" check before proceeding.
*/
export async function requireAdminUser(): Promise<AuthGuardResult> {
const adminUser = await getAdminUser();
if (!adminUser) {
return { authorized: false, adminUser: null };
}
return { authorized: true, adminUser };
}
+77
View File
@@ -0,0 +1,77 @@
// FedEx OAuth token cache.
// This module intentionally lives OUTSIDE any "use server" file. The
// `react-doctor/server-no-mutable-module-state` rule flags module-scope
// mutable state inside "use server" files because it can leak per-user
// state across requests. The FedEx token cache is NOT per-user state —
// it's a service-credential cache shared across all requests, which is
// the desired behavior. Co-locating the cache in a non-server file keeps
// the rule satisfied while preserving the cross-request token reuse that
// FedEx's rate-limited OAuth endpoint requires.
type FedExAuthToken = {
accessToken: string;
expiresAt: number;
};
const FEDEX_BASE_URL = "https://apis.fedex.com";
const FEDEX_SANDBOX_URL = "https://apis-sandbox.fedex.com";
export function getFedExApiBaseUrl(useProduction: boolean): string {
return useProduction ? FEDEX_BASE_URL : FEDEX_SANDBOX_URL;
}
// Buffer before expiry to refresh proactively
const EXPIRY_BUFFER_MS = 5 * 60 * 1000;
let cachedToken: FedExAuthToken | null = null;
export async function getFedExAuthToken(settings: {
fedexApiKey: string;
fedexApiSecret: string;
useProduction: boolean;
}): Promise<{ accessToken: string } | { error: string }> {
const base = settings.useProduction ? FEDEX_BASE_URL : FEDEX_SANDBOX_URL;
if (cachedToken && Date.now() < cachedToken.expiresAt - EXPIRY_BUFFER_MS) {
return { accessToken: cachedToken.accessToken };
}
const credentials = Buffer.from(
`${settings.fedexApiKey}:${settings.fedexApiSecret}`
).toString("base64");
try {
const res = await fetch(`${base}/oauth/token`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${credentials}`,
},
body: "grant_type=client_credentials",
});
if (!res.ok) {
const text = await res.text();
return { error: `FedEx auth failed: ${text}` };
}
const data = (await res.json()) as {
access_token: string;
expires_in: number;
};
cachedToken = {
accessToken: data.access_token,
expiresAt: Date.now() + data.expires_in * 1000,
};
return { accessToken: data.access_token };
} catch (err) {
return { error: `FedEx auth network error: ${(err as Error).message}` };
}
}
// Test-only helper to reset the cache between unit tests.
export function __resetFedExTokenCacheForTests(): void {
cachedToken = null;
}