Merge branch 'feature/drizzle-rls-real-auth'

# Conflicts:
#	CLAUDE.md
#	package.json
#	src/app/api/auth/[...nextauth]/route.ts
#	src/app/login/LoginClient.tsx
#	src/auth.config.ts
#	src/components/admin/AdminSidebar.tsx
#	src/lib/admin-permissions-types.ts
#	src/lib/admin-permissions.ts
#	src/lib/auth.ts
#	src/middleware.ts
This commit is contained in:
2026-06-07 01:55:06 +00:00
84 changed files with 4165 additions and 2881 deletions
-60
View File
@@ -1,60 +0,0 @@
import { cookies } from "next/headers";
export default async function DebugAuthPage() {
const cookieStore = await cookies();
const allCookies = cookieStore.getAll();
const rcAuthUid = cookieStore.get("rc_auth_uid")?.value;
const rcUid = cookieStore.get("rc_uid")?.value;
const rcAccessToken = cookieStore.get("rc_access_token")?.value;
let adminUsersStatus = "not_tried";
let adminUsersResult: string | null = null;
if (rcAuthUid) {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (supabaseUrl && serviceKey) {
try {
const res = await fetch(
`${supabaseUrl}/rest/v1/admin_users?user_id=eq.${rcAuthUid}&limit=1`,
{ headers: { apikey: serviceKey, "Content-Type": "application/json" } }
);
adminUsersStatus = String(res.status);
const data = await res.json().catch(() => null);
adminUsersResult = JSON.stringify(data);
} catch (e) {
adminUsersStatus = "error: " + (e instanceof Error ? e.message : String(e));
}
} else {
adminUsersStatus = "missing_env_vars";
}
} else {
adminUsersStatus = "no_rc_auth_uid_cookie";
}
return (
<div className="min-h-screen bg-zinc-950 p-8 text-white">
<h1 className="text-2xl font-bold text-emerald-400 mb-6">Auth Debug</h1>
<div className="mb-6">
<h2 className="text-xs font-semibold text-stone-400 uppercase mb-2">Cookies ({allCookies.length})</h2>
<pre className="bg-black/50 p-4 rounded-xl text-xs overflow-auto max-h-64">
{allCookies.map(c => `${c.name}=${c.value.slice(0, 80)}${c.value.length > 80 ? "..." : ""}`).join("\n") || "(none)"}
</pre>
</div>
<div className="mb-6">
<h2 className="text-xs font-semibold text-stone-400 uppercase mb-2">Key Cookies</h2>
<p>rc_auth_uid: <span className={rcAuthUid ? "text-emerald-400" : "text-red-400"}>{rcAuthUid ? `${rcAuthUid.slice(0, 20)}...` : "NOT SET"}</span></p>
<p>rc_uid: <span className={rcUid ? "text-emerald-400" : "text-red-400"}>{rcUid ? `${rcUid.slice(0, 20)}...` : "NOT SET"}</span></p>
<p>rc_access_token: <span className={rcAccessToken ? "text-yellow-400" : "text-red-400"}>{rcAccessToken ? "PRESENT" : "NOT SET"}</span></p>
</div>
<div className="mb-6">
<h2 className="text-xs font-semibold text-stone-400 uppercase mb-2">Admin Users Lookup</h2>
<p>Status: <span className="text-lg font-mono">{adminUsersStatus}</span></p>
<p>Result: <pre className="bg-black/50 p-4 rounded-xl text-xs overflow-auto mt-2">{adminUsersResult || "(none)"}</pre></p>
</div>
</div>
);
}
+10 -41
View File
@@ -2,9 +2,7 @@
import { useState } from "react";
import Link from "next/link";
import { supabase } from "@/lib/supabase";
import { AdminUserRow } from "@/actions/admin/users";
import { logUserActivity } from "@/actions/admin/audit";
type ProfilePageProps = {
currentUser: AdminUserRow;
@@ -21,53 +19,24 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) {
const [newEmail, setNewEmail] = useState("");
const [emailError, setEmailError] = useState<string | null>(null);
// Profile / email mutations used to call Supabase directly. With the
// platform moved off Supabase entirely, those handlers are stubbed out
// — the page remains read-only until a server-action equivalent ships.
// See the final YOLO report for the broader Supabase → pg data-fetch
// migration that covers the rest of the admin pages.
async function handleSaveProfile(e: React.FormEvent) {
e.preventDefault();
setSaving(true);
setError(null);
try {
const { error: rpcError } = await supabase.rpc("update_admin_user", {
p_id: currentUser.id,
p_display_name: displayName || null,
p_phone_number: phoneNumber || null,
});
if (rpcError) {
setError(rpcError.message);
return;
}
await logUserActivity({
user_id: currentUser.user_id,
activity_type: "profile_update",
details: { fields: ["display_name", "phone_number"] },
});
setEditing(false);
} finally {
setSaving(false);
}
setError("Profile editing is temporarily unavailable. Contact a platform admin.");
setSaving(false);
}
async function handleEmailChange(e: React.FormEvent) {
e.preventDefault();
setChangingEmail(true);
setEmailError(null);
try {
const { error: updateError } = await supabase.auth.updateUser({
email: newEmail,
});
if (updateError) {
setEmailError(updateError.message);
return;
}
await logUserActivity({
user_id: currentUser.user_id,
activity_type: "email_change",
details: { new_email: newEmail },
});
setEmailChangeSent(true);
setChangingEmail(false);
} finally {
setChangingEmail(false);
}
setEmailError("Email changes are temporarily unavailable. Contact a platform admin.");
setChangingEmail(false);
}
return (
@@ -4,6 +4,10 @@ import { getPaymentSettings } from "@/actions/payments";
import { getSyncLog, type SyncLogEntry } from "@/actions/square-sync-ui";
import SquareSyncSettingsClient from "./SquareSyncSettingsClient";
// Uses cookies() via getAdminUser — must be dynamic to avoid the
// "couldn't be rendered statically" build error.
export const dynamic = "force-dynamic";
export default async function SquareSyncSettingsPage() {
const adminUser = await getAdminUser();
if (!adminUser) redirect("/login");
-90
View File
@@ -1,90 +0,0 @@
import { getAdminUser } from "@/lib/admin-permissions";
import { cookies } from "next/headers";
export const dynamic = "force-dynamic";
export default async function TestAuthPage() {
const cookieStore = await cookies();
const allCookies = cookieStore.getAll();
let adminUser = null;
let error: string | null = null;
try {
adminUser = await getAdminUser();
} catch (e: unknown) {
error = e instanceof Error ? e.message : String(e);
}
return (
<div className="min-h-screen bg-zinc-950 p-8 text-white">
<h1 className="text-2xl font-bold mb-6 text-emerald-400">Auth Debug</h1>
<div className="mb-6">
<h2 className="text-xs font-semibold text-stone-400 uppercase mb-2">All Cookies ({allCookies.length})</h2>
<pre className="bg-black/50 p-4 rounded-xl text-xs overflow-auto max-h-64">
{allCookies.map(c => `${c.name}=${c.value.slice(0, 80)}${c.value.length > 80 ? "..." : ""}`).join("\n") || "(none)"}
</pre>
</div>
<div className="mb-6">
<h2 className="text-xs font-semibold text-stone-400 uppercase mb-2">rc_auth_uid</h2>
<p className="text-lg font-mono">
{allCookies.some(c => c.name === "rc_auth_uid")
? <span className="text-emerald-400">SET {allCookies.find(c => c.name === "rc_auth_uid")?.value}</span>
: <span className="text-red-400">NOT SET</span>
}
</p>
</div>
<div className="mb-6">
<h2 className="text-xs font-semibold text-stone-400 uppercase mb-2">rc_access_token</h2>
<p className="text-lg font-mono">
{allCookies.some(c => c.name === "rc_access_token")
? <span className="text-yellow-400">SET (not needed)</span>
: <span className="text-zinc-500">NOT SET (OK)</span>
}
</p>
</div>
<div className="mb-6">
<h2 className="text-xs font-semibold text-stone-400 uppercase mb-2">getAdminUser() result</h2>
{error ? (
<div>
<p className="text-red-400 font-bold">ERROR</p>
<pre className="bg-red-950/50 p-4 rounded-xl text-sm mt-2">{error}</pre>
</div>
) : adminUser ? (
<div>
<p className="text-emerald-400 font-bold">AUTHENTICATED</p>
<pre className="bg-black/50 p-4 rounded-xl text-sm mt-2 overflow-auto">
{JSON.stringify({
id: adminUser.id,
user_id: adminUser.user_id,
role: adminUser.role,
brand_id: adminUser.brand_id,
active: adminUser.active,
}, null, 2)}
</pre>
</div>
) : (
<p className="text-red-400">NOT AUTHENTICATED null returned</p>
)}
</div>
<div className="mt-8 pt-4 border-t border-zinc-800">
<h2 className="text-xs font-semibold text-stone-400 uppercase mb-2">Quick Actions</h2>
<div className="flex gap-4">
<a href="/admin" className="px-4 py-2 bg-emerald-600 text-white rounded-xl text-sm font-bold hover:bg-emerald-500">
Go to Admin
</a>
<form action="/api/logout" method="POST">
<button type="submit" className="px-4 py-2 bg-zinc-800 text-white rounded-xl text-sm font-bold hover:bg-zinc-700">
Logout
</button>
</form>
</div>
</div>
</div>
);
}
-51
View File
@@ -1,51 +0,0 @@
import { getAdminUser } from "@/lib/admin-permissions";
import { cookies } from "next/headers";
export const dynamic = "force-dynamic";
export default async function TestPage() {
const cookieStore = await cookies();
const allCookies = cookieStore.getAll();
let adminUser = null;
let adminUserError: string | null = null;
try {
adminUser = await getAdminUser();
} catch (e: any) {
adminUserError = e?.message ?? String(e);
}
return (
<div className="min-h-screen bg-zinc-950 p-8 text-white">
<h1 className="text-2xl font-bold mb-6 text-emerald-400">Auth Debug</h1>
<div className="mb-6">
<h2 className="text-xs font-semibold text-stone-400 uppercase mb-2">Server cookies ({allCookies.length})</h2>
<pre className="bg-black/50 p-4 rounded-xl text-xs overflow-auto max-h-64">
{allCookies.map(c => `${c.name}=${c.value.slice(0, 80)}...`).join("\n") || "(none)"}
</pre>
</div>
<div className="mb-6">
<h2 className="text-xs font-semibold text-stone-400 uppercase mb-2">rc_access_token present?</h2>
<p className="text-lg font-mono">
{allCookies.some(c => c.name === "rc_access_token")
? <span className="text-emerald-400">YES {allCookies.find(c => c.name === "rc_access_token")?.value.length} chars</span>
: <span className="text-red-400">NO</span>
}
</p>
</div>
<div>
<h2 className="text-xs font-semibold text-stone-400 uppercase mb-2">getAdminUser() result</h2>
{adminUserError ? (
<p className="text-red-400">ERROR: {adminUserError}</p>
) : (
<pre className="bg-black/50 p-4 rounded-xl text-sm overflow-auto">
{JSON.stringify(adminUser, null, 2)}
</pre>
)}
</div>
</div>
);
}