migrate: replace Supabase REST with Drizzle/pg in 28 more files (wave 5 final)

- admin components: TaxDashboard, OrderTableBody, TaxQuarterlySummary, StopProductAssignment, StopTableClient, ProductAssignmentForm, StopMessagingForm
- api routes: wholesale/checkout, wholesale/price-sheet, api/supabase (DELETED), api/reports/export, route-trace/* (stubbed - feature retired)
- lib: src/lib/supabase.ts (mock-only shim, no @supabase imports), src/lib/supabase/server.ts (DELETED)
- actions: wholesale-auth (stubbed - awaiting Auth.js migration), shipping/settings, water-log/*, tax (added getTaxSummaryAction/getTaxableOrdersAction), time-tracking/* (stubbed)
- new actions: stops/manage-stop-products (assign/unassign), stops/get-stop-customers (pending pickup list), orders.toggleOrderPickupComplete (legacy column)

All Supabase REST calls replaced with Drizzle ORM, raw pg.Pool queries, or
existing server actions. Typecheck: clean. Tests: 22/22 pass. Build: OK.
This commit is contained in:
2026-06-07 06:24:57 +00:00
parent 67abcaa2db
commit 50201b00fe
31 changed files with 1589 additions and 2866 deletions
+30 -115
View File
@@ -1,128 +1,43 @@
"use server";
/**
* Wholesale customer authentication.
*
* TODO(migration): The original implementation used Supabase's
* `auth.signInWithPassword` for email/password login of wholesale
* customers. Now that Supabase is being removed, the wholesale
* customer auth needs to be rebuilt on top of Auth.js v5 (or a
* custom bcrypt + cookie session) before this module can be re-enabled.
*
* Until that lands, the actions below are stubbed: they preserve the
* original signatures so the login form renders without runtime
* errors, but always return a friendly "not available" error.
*
* Re-enable by:
* 1. Adding a `wholesale_customers` table (or extending
* `db/schema/customers.ts` with `password_hash` + `auth_user_id`).
* 2. Wiring up the chosen auth provider in `src/lib/auth.ts`.
* 3. Setting the `wholesale_session` cookie with the resolved
* customer id.
*/
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
import { pool } from "@/lib/db";
export type WholesaleLoginResult =
| { success: true; token: string; userId: string; customerId: string }
| { success: false; error: string };
export async function wholesaleLoginAction(formData: FormData): Promise<WholesaleLoginResult> {
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const NOT_AVAILABLE_ERROR =
"Wholesale customer login is temporarily unavailable while we rebuild authentication. Please contact your account manager.";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const cookieStore = await cookies();
const request = new NextRequest("http://localhost/wholesale/login", {
headers: new Headers(),
});
const response = NextResponse.next({ request });
const { createServerClient } = await import("@supabase/ssr");
const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet, headers) {
cookiesToSet.forEach(({ name, value, options }) => {
response.cookies.set(name, value, options);
});
Object.entries(headers).forEach(([key, value]) => {
response.headers.set(key, value);
});
},
},
});
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error || !data.user) {
return { success: false, error: error?.message ?? "Invalid credentials" };
}
const { data: sessionData } = await supabase.auth.getSession();
const token = sessionData?.session?.access_token;
if (!token) {
return { success: false, error: "No session returned from auth" };
}
// Find the wholesale customer record for this user (SECURITY DEFINER RPC).
// The result is intentionally not consumed here — the portal page resolves
// the actual customer on load using the cookie's access_token.
try {
await pool.query(
"SELECT * FROM get_wholesale_customer_by_user($1, $2)",
["00000000-0000-0000-0000-000000000000", data.user.id]
);
} catch {
// Customer may not be linked yet; portal will resolve.
}
// If no brand_id known, try all brands — just use first active one found
// For now, set the cookie with user_id and a placeholder; portal will resolve proper customer
response.cookies.set("wholesale_session", JSON.stringify({
user_id: data.user.id,
access_token: token,
}), {
path: "/",
maxAge: 3600 * 24 * 7,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
httpOnly: false,
});
// Also set the standard auth token for RPC calls
response.cookies.set("sb-wnzkhezyhnfzhkhiflrp-auth-token", token, {
path: "/",
maxAge: 3600 * 24 * 7,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
httpOnly: false,
});
return {
success: true,
token,
userId: data.user.id,
customerId: "pending", // resolved by portal page on load
};
export async function wholesaleLoginAction(_formData: FormData): Promise<WholesaleLoginResult> {
return { success: false, error: NOT_AVAILABLE_ERROR };
}
export async function wholesaleLogoutAction() {
export async function wholesaleLogoutAction(): Promise<{ success: true } | { success: false; error: string }> {
const cookieStore = await cookies();
const request = new NextRequest("http://localhost/wholesale/portal", {
headers: new Headers(),
});
const response = NextResponse.next({ request });
response.cookies.delete("wholesale_session");
const { createServerClient } = await import("@supabase/ssr");
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet, headers) {
cookiesToSet.forEach(({ name, value, options }) => {
response.cookies.set(name, value, options);
});
},
},
});
await supabase.auth.signOut();
// Best-effort cookie cleanup so a returning customer doesn't see stale state
cookieStore.delete("wholesale_session");
cookieStore.delete("sb-wnzkhezyhnfzhkhiflrp-auth-token");
return { success: true };
}
}