fix(auth): port dev auto-login to Next.js 16 proxy.ts
Deploy to route.crispygoat.com / deploy (push) Successful in 3m8s

Next.js 16 renamed middleware.ts to proxy.ts and only allows one of the
two. Delete src/middleware.ts and rewrite src/proxy.ts to do the dev
auto-login + /login bounce + /admin gate explicitly (no NextAuth auth()
wrapper — auth.handler() returns a NextMiddleware taking (req, event),
which makes wrapping it awkward when we also need to set cookies on the
response).

Drop the now-dead 'authorized' callback from auth.config.ts (it was only
fired by the NextAuth wrapper, which we no longer use).

Migration 209: add a defensive unique constraint on admin_users.user_id
so the ON CONFLICT (user_id) clause in the new RPC resolves regardless
of whether the Supabase-dashboard-created table shipped with one.
This commit is contained in:
2026-06-06 21:52:19 +00:00
parent b63d0415ab
commit 1cecbce392
4 changed files with 101 additions and 127 deletions
+72 -15
View File
@@ -1,26 +1,83 @@
import NextAuth from "next-auth";
import { authConfig } from "@/auth.config";
import { NextResponse, type NextRequest } from "next/server";
/**
* Root-level proxy (formerly `middleware.ts`, renamed in Next.js 16).
* This is the single source of truth for route protection. The legacy
* `src/middleware.ts` has been deleted (Next.js only runs one).
*
* Why an `auth` wrapper instead of a hand-rolled `NextResponse.next()`?
* 1. Auth.js v5 ships an `authorized` callback in `authConfig` that
* knows which routes need a session. We reuse it here at the edge.
* 2. It auto-populates `request.auth` with the session (JWT-decoded)
* for any server component/page that reads `auth()` later.
* Routing policy:
* 1. `/admin/*` with no auth cookie + `ALLOW_DEV_LOGIN !== "false"`
* → set `dev_session=platform_admin` and let the request through.
* This makes `/admin` "just work" in dev/demo without any login
* UI gymnastics.
* 2. `/login` with an auth cookie (any flavour)
* → redirect to `/admin` so an authenticated user never sees the
* login form.
* 3. `/admin/*` (or `/protected-example`) with no auth cookie
* → redirect to `/login`.
* 4. Everything else → continue.
*
* Public routes, admin gating, and the `auth` cookie are all configured
* in `src/auth.config.ts`.
* Auth-cookie flavours recognised:
* - `dev_session` (dev auto-login, see above)
* - `rc_auth_uid` / `rc_uid` (legacy /api/login flow)
* - `authjs.session-token` / `__Secure-authjs.session-token` (Auth.js v5)
*
* The proxy only checks cookie *presence*. The real auth check (JWT
* signature decryption, admin role lookup) happens in
* `getAdminUser()` server-side. The proxy is just routing.
*/
const { auth } = NextAuth(authConfig);
export default auth;
function isAuthenticated(request: NextRequest): boolean {
const dev = request.cookies.get("dev_session")?.value;
if (
dev === "platform_admin" ||
dev === "brand_admin" ||
dev === "store_employee"
) {
return true;
}
if (request.cookies.get("rc_auth_uid")?.value) return true;
if (request.cookies.get("rc_uid")?.value) return true;
if (request.cookies.get("authjs.session-token")?.value) return true;
if (request.cookies.get("__Secure-authjs.session-token")?.value) return true;
return false;
}
export default function proxy(request: NextRequest) {
const { nextUrl } = request;
const isOnAdmin = nextUrl.pathname.startsWith("/admin");
const isOnProtectedExample = nextUrl.pathname.startsWith(
"/protected-example"
);
const isOnLogin = nextUrl.pathname === "/login";
const authenticated = isAuthenticated(request);
// ── 1. Dev auto-login for /admin/* ───────────────────────────────
if (isOnAdmin && !authenticated) {
const allowDev = process.env.ALLOW_DEV_LOGIN !== "false";
if (allowDev) {
const response = NextResponse.next();
response.cookies.set("dev_session", "platform_admin", {
path: "/",
maxAge: 60 * 60 * 24, // 1 day
sameSite: "lax",
});
return response;
}
}
// ── 2. Bounce authenticated users away from /login ───────────────
if (isOnLogin && isAuthenticated(request)) {
return NextResponse.redirect(new URL("/admin", nextUrl));
}
// ── 3. Gate protected routes ─────────────────────────────────────
if ((isOnAdmin || isOnProtectedExample) && !authenticated) {
return NextResponse.redirect(new URL("/login", nextUrl));
}
// ── 4. Everything else: continue ─────────────────────────────────
return NextResponse.next();
}
export const config = {
// Run on /admin and the protected example, plus /login so the
// `authorized` callback can bounce already-signed-in users away from it.
matcher: ["/admin/:path*", "/admin", "/login", "/protected-example"],
};