chore(auth): remove legacy rc_auth_uid/rc_uid/upsert_admin_user path; scope sign-in to ADMIN_ALLOWED_EMAILS; silence lockfile warning
Deploy to route.crispygoat.com / deploy (push) Successful in 3m14s

Cleanup after Auth.js v5 became the only sign-in path. The platform
had three overlapping auth modes (dev cookie, legacy rc_auth_uid, Auth.js
JWT) and a pile of dead-code pages/routes that only existed to support
the legacy path.

What changed:

* getAdminUser() now has only two auth paths:
    1. dev_session cookie (auto-issued by src/proxy.ts for /admin/* when
       ALLOW_DEV_LOGIN is enabled)
    2. Auth.js v5 JWT (the encrypted cookie + auth() lookup)
  The legacy rc_auth_uid/rc_uid branch and the Supabase REST fetch
  against admin_users are gone.

* The signIn callback in src/lib/auth.ts enforces ADMIN_ALLOWED_EMAILS
  when set. Unset = open mode (backward compatible with demo/dev). Dev
  credentials provider is exempt. The new env var is wired through
  .env.example and .gitea/workflows/deploy.yml (read from
  secrets.ADMIN_ALLOWED_EMAILS, written to the server .env file).

* change-password/page.tsx now uses auth() server-side instead of
  fetching the deleted /api/auth/uid endpoint. The form is split into
  page.tsx (server component, auth check) + ChangePasswordForm.tsx
  (client component, form state). updatePasswordAction now reads the
  user id from auth() instead of the rc_auth_uid cookie.

* Deleted 14 dead-code files:
    - Pages: login2, logout, auth/callback, admin/debug-auth,
      admin/test-auth
    - API routes: api/login, api/logout, api/auth/uid, api/force-admin,
      api/set-auth-cookie, api/debug-cookie, api/debug-me,
      api/debug-auth
    - Actions: src/actions/login.ts
  These were the old email/password login, the old Supabase OAuth
  callback, the old /api/auth/uid probe, and a pile of debug endpoints
  that have been superseded by the new proxy + the new /login page.

* next.config.ts: set outputFileTracingRoot: '.' to silence the
  Next.js 16 lockfile-inference warning. Without this the build
  walked up from package.json looking for a lockfile, found the
  homelab runner's stale act cache at /home/tyler/.cache/act/.../package-lock.json,
  and warned on every build. '. resolves to the project root in both
  dev and CI, so it's the right answer.

Out of scope (deferred):

* src/actions/admin/users.ts still uses rc_auth_uid internally for its
  dev-bypass logic. It works (the rc_auth_uid branch is gated on
  NODE_ENV != 'production' and DEV_FORCE_UID), but it's now genuinely
  unreachable in production. Clean up in a follow-up.

Pre-flight:
* npx tsc --noEmit: clean
* npm run lint (touched files): clean
* npm run build: clean — proxy picked up, no lockfile warning, all
  93 static pages generated.
This commit is contained in:
2026-06-06 22:13:56 +00:00
parent 1cecbce392
commit 5654ebaecd
22 changed files with 229 additions and 916 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>
);
}
-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>
);
}