fix(auth): port dev auto-login to Next.js 16 proxy.ts
Deploy to route.crispygoat.com / deploy (push) Successful in 3m8s
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:
@@ -44,32 +44,6 @@ export const authConfig = {
|
||||
session: { strategy: "jwt" },
|
||||
|
||||
callbacks: {
|
||||
/**
|
||||
* Gate /admin routes. Anything not on the public list and not signed in
|
||||
* gets redirected to /login. This mirrors what the page-level checks do,
|
||||
* but runs first at the edge so unauthorized requests never hit the
|
||||
* server component tree.
|
||||
*/
|
||||
authorized({ auth, request: { nextUrl } }) {
|
||||
const isLoggedIn = !!auth?.user;
|
||||
const isOnAdmin = nextUrl.pathname.startsWith("/admin");
|
||||
const isOnProtectedExample = nextUrl.pathname.startsWith(
|
||||
"/protected-example"
|
||||
);
|
||||
|
||||
if (isOnAdmin) {
|
||||
if (isLoggedIn) return true;
|
||||
return false; // Redirect to /login
|
||||
}
|
||||
|
||||
if (isOnProtectedExample) {
|
||||
if (isLoggedIn) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Forward the user id from the database user record into the JWT on
|
||||
* initial sign-in. With database sessions this is what populates
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
|
||||
/**
|
||||
* Middleware for /admin/* and /login routes.
|
||||
*
|
||||
* Recognises three auth sources at the edge:
|
||||
* 1. Auth.js v5 JWT cookie (`authjs.session-token` or the `__Secure-`
|
||||
* variant in prod) — set after Google sign-in.
|
||||
* 2. `dev_session`, `rc_auth_uid`, `rc_uid` — legacy / dev cookies.
|
||||
* 3. Demo / dev auto-login — when ALLOW_DEV_LOGIN is enabled (on by
|
||||
* default in non-prod) and the user has no auth cookie, automatically
|
||||
* issue `dev_session=platform_admin` so visiting /admin just works.
|
||||
* No buttons, no demo page, no client-side cookie games.
|
||||
*
|
||||
* This is the single source of truth for "am I allowed in?" at the edge.
|
||||
* The page-level `getAdminUser()` re-checks the same cookies (and reads
|
||||
* the Auth.js JWT) to resolve the role.
|
||||
*/
|
||||
export function middleware(request: NextRequest) {
|
||||
const { nextUrl } = request;
|
||||
const isOnAdmin = nextUrl.pathname.startsWith("/admin");
|
||||
const isOnLogin = nextUrl.pathname === "/login";
|
||||
|
||||
// Read auth cookies once.
|
||||
const dev = request.cookies.get("dev_session")?.value;
|
||||
const rcAuthUid = request.cookies.get("rc_auth_uid")?.value;
|
||||
const rcUid = request.cookies.get("rc_uid")?.value;
|
||||
// Auth.js v5 sets an encrypted JWT cookie. Names differ by transport:
|
||||
// `authjs.session-token` (dev / HTTP) and `__Secure-authjs.session-token`
|
||||
// (prod / HTTPS). The presence of either cookie is sufficient to
|
||||
// consider the request authenticated at the edge — the page-level
|
||||
// `getAdminUser()` re-reads the JWT and resolves the role.
|
||||
const authJsSessionToken = request.cookies.get("authjs.session-token")?.value;
|
||||
const authJsSecureToken = request.cookies.get("__Secure-authjs.session-token")?.value;
|
||||
const hasDevSession =
|
||||
dev === "platform_admin" ||
|
||||
dev === "brand_admin" ||
|
||||
dev === "store_employee";
|
||||
const hasRealAuth = Boolean(rcAuthUid || rcUid);
|
||||
const hasAuthJsSession = Boolean(authJsSessionToken || authJsSecureToken);
|
||||
const isAuthenticated = hasDevSession || hasRealAuth || hasAuthJsSession;
|
||||
|
||||
// ── /admin/* ──────────────────────────────────────────────────────
|
||||
if (isOnAdmin) {
|
||||
if (isAuthenticated) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// No auth cookie — try demo auto-login. ALLOW_DEV_LOGIN is on by
|
||||
// default; set it to "false" in prod env to disable.
|
||||
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;
|
||||
}
|
||||
|
||||
// No auth and dev disabled — bounce to /login.
|
||||
const loginUrl = nextUrl.clone();
|
||||
loginUrl.pathname = "/login";
|
||||
loginUrl.search = "";
|
||||
return NextResponse.redirect(loginUrl);
|
||||
}
|
||||
|
||||
// ── /login ────────────────────────────────────────────────────────
|
||||
if (isOnLogin) {
|
||||
// If already authenticated, skip the login page.
|
||||
if (isAuthenticated) {
|
||||
const adminUrl = nextUrl.clone();
|
||||
adminUrl.pathname = "/admin";
|
||||
adminUrl.search = "";
|
||||
return NextResponse.redirect(adminUrl);
|
||||
}
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/admin/:path*", "/login"],
|
||||
};
|
||||
+72
-15
@@ -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"],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user