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
+118
View File
@@ -0,0 +1,118 @@
import { NextResponse } from "next/server";
import { pool } from "@/lib/db";
function escapeAttr(s: string): string {
return s
.replace(/&/g, "&")
.replace(/"/g, """)
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
export async function POST(request: Request) {
try {
const form = await request.formData();
const code = String(form.get("code") ?? "");
const state = String(form.get("state") ?? "");
const error = String(form.get("error") ?? "");
const originHeader = request.headers.get("origin");
const baseUrl = originHeader ?? new URL(request.url).origin;
if (error) {
return NextResponse.redirect(
new URL(
`/admin/settings/payments?error=square_oauth_denied&reason=${encodeURIComponent(error)}`,
baseUrl,
),
);
}
if (!code || !state) {
return NextResponse.redirect(
new URL("/admin/settings/payments?error=square_oauth_missing_params", baseUrl),
);
}
let brandId: string | null = null;
try {
const decoded = JSON.parse(Buffer.from(state, "base64").toString("utf-8"));
brandId = decoded?.brandId ?? null;
} catch {
return NextResponse.redirect(
new URL("/admin/settings/payments?error=square_oauth_invalid_state", baseUrl),
);
}
if (!brandId) {
return NextResponse.redirect(
new URL("/admin/settings/payments?error=square_oauth_missing_state", baseUrl),
);
}
const appId = process.env.NEXT_PUBLIC_SQUARE_APP_ID;
const appSecret = process.env.SQUARE_APP_SECRET;
const env = process.env.SQUARE_ENVIRONMENT ?? "sandbox";
if (!appId || !appSecret) {
return NextResponse.redirect(
new URL("/admin/settings/payments?error=square_credentials_not_configured", baseUrl),
);
}
const tokenUrl =
env === "production"
? "https://connect.squareup.com/v2/oauth2/token"
: "https://connect.squareupsandbox.com/v2/oauth2/token";
const redirectUri = `${baseUrl}/api/square/oauth/callback`;
const tokenResponse = await fetch(tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Square-Version": "2025-01-16",
},
body: JSON.stringify({
client_id: appId,
client_secret: appSecret,
code,
grant_type: "authorization_code",
redirect_uri: redirectUri,
}),
});
const tokenData = await tokenResponse.json();
const accessToken: string | null = tokenData?.access_token ?? null;
const locationId: string | null = tokenData?.location_id ?? null;
if (!tokenResponse.ok || !accessToken) {
return NextResponse.redirect(
new URL("/admin/settings/payments?error=square_token_exchange_failed", baseUrl),
);
}
await pool.query(
"SELECT upsert_payment_settings($1, $2, $3, $4, $5, $6, $7, $8, $9)",
[
brandId,
"square",
null,
null,
null,
accessToken,
locationId,
null,
null,
],
);
return NextResponse.redirect(
new URL("/admin/settings/payments?square_connected=true", baseUrl),
);
} catch (err) {
const baseUrl = request.headers.get("origin") ?? new URL(request.url).origin;
return NextResponse.redirect(
new URL("/admin/settings/payments?error=square_token_save_error", baseUrl),
);
}
}
export async function GET() {
// GET on /complete is not supported; redirect to settings.
return NextResponse.redirect(new URL("/admin/settings/payments"));
}
+102
View File
@@ -0,0 +1,102 @@
import { NextResponse } from "next/server";
import { getAdminUser } from "@/lib/admin-permissions";
import { savePaymentSettings } from "@/actions/payments";
export async function POST(request: Request) {
try {
const form = await request.formData();
const code = String(form.get("code") ?? "");
const error = String(form.get("error") ?? "");
const state = String(form.get("state") ?? "");
const baseUrl = request.headers.get("origin") ?? new URL(request.url).origin;
if (error) {
return NextResponse.redirect(
new URL(`/admin/settings/payments?stripe_error=${encodeURIComponent(error)}`, baseUrl),
);
}
if (!code) {
return NextResponse.redirect(
new URL("/admin/settings/payments?stripe_error=no_code", baseUrl),
);
}
const adminUser = await getAdminUser();
if (!adminUser || !adminUser.brand_id) {
return NextResponse.redirect(new URL("/admin/settings/payments?error=unauthorized", baseUrl));
}
let brandId = adminUser.brand_id;
if (state) {
try {
const decoded = JSON.parse(Buffer.from(state, "base64").toString());
if (decoded.brandId) brandId = decoded.brandId;
} catch {
// ignore malformed state
}
}
const clientId = process.env.STRIPE_CLIENT_ID;
const clientSecret = process.env.STRIPE_CLIENT_SECRET;
if (!clientId || !clientSecret) {
return NextResponse.redirect(
new URL("/admin/settings/payments?stripe_error=oauth_not_configured", baseUrl),
);
}
const tokenResponse = await fetch("https://connect.stripe.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
client_id: clientId,
client_secret: clientSecret,
}),
});
const tokenData = await tokenResponse.json();
if (tokenData?.error) {
return NextResponse.redirect(
new URL(
`/admin/settings/payments?stripe_error=${encodeURIComponent(tokenData.error_description || tokenData.error)}`,
baseUrl,
),
);
}
const stripeUserId: string | undefined = tokenData.stripe_user_id;
const accessToken: string | undefined = tokenData.access_token;
const publishableKey: string | undefined = tokenData.stripe_publishable_key;
const result = await savePaymentSettings({
brandId,
provider: "stripe",
stripePublishableKey: publishableKey,
stripeSecretKey: accessToken,
stripeUserId,
});
if (result.success) {
return NextResponse.redirect(
new URL("/admin/settings/payments?stripe_connected=true", baseUrl),
);
}
return NextResponse.redirect(
new URL(
`/admin/settings/payments?stripe_error=${encodeURIComponent(result.error || "Failed to save")}`,
baseUrl,
),
);
} catch (err) {
const baseUrl = request.headers.get("origin") ?? new URL(request.url).origin;
return NextResponse.redirect(
new URL(
`/admin/settings/payments?stripe_error=${encodeURIComponent("Failed to complete OAuth")}`,
baseUrl,
),
);
}
}
export async function GET() {
return NextResponse.redirect(new URL("/admin/settings/payments"));
}