feat(auth): opt-in local auto-login for single-user deployments
Skips the login form and lands on the dashboard. It performs a REAL backend login — the API still issues and can revoke the session — so this does not weaken auth; it only removes a form for a deployment with exactly one operator. Gated on BOTH LOCAL_AUTOLOGIN_EMAIL and LOCAL_AUTOLOGIN_PASSWORD, and refuses outright in clerk mode. Prod sets neither, so the route 404s there. Two conditions rather than one flag: a single misread value should not be able to hand a session to an anonymous visitor. The route emits a RELATIVE Location — inside the container request.url is the 0.0.0.0:3000 bind, so NextResponse.redirect would send the browser to a host that only exists in Docker — and the cookie's secure flag keys on x-forwarded-proto rather than NODE_ENV. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7cf77a9248
commit
022ef98e44
@@ -0,0 +1,68 @@
|
||||
// LOCAL-ONLY auto-login. Mints a real session for a fixed operator so a
|
||||
// single-user local stack lands on the dashboard instead of the login form.
|
||||
//
|
||||
// This is a genuine login against the Rust backend — the same call
|
||||
// /auth/session makes — not an auth bypass in the server. The backend still
|
||||
// issues (and can revoke) the session, so nothing here weakens API auth.
|
||||
//
|
||||
// OFF unless LOCAL_AUTOLOGIN_EMAIL *and* LOCAL_AUTOLOGIN_PASSWORD are both
|
||||
// set. Prod (Clerk) never sets them, and `authMode() === "clerk"` refuses
|
||||
// outright, so this route 404s everywhere but the local box. It is deliberately
|
||||
// two conditions: a single misread flag should not be able to hand a session to
|
||||
// an anonymous visitor.
|
||||
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
|
||||
import { apiOrigin, TOKEN_COOKIE } from "@/lib/api/http";
|
||||
import { authMode } from "@/lib/auth/mode";
|
||||
|
||||
const SESSION_MAX_AGE_SECONDS = 7 * 24 * 60 * 60;
|
||||
|
||||
// A RELATIVE Location, deliberately. NextResponse.redirect() needs an absolute
|
||||
// URL, and inside the container `request.url` is the 0.0.0.0:3000 bind — so
|
||||
// behind `tailscale serve` it would redirect the browser to a host that only
|
||||
// exists inside Docker. A relative Location (RFC 7231 §7.1.2) lets the browser
|
||||
// resolve against whatever origin it actually asked for.
|
||||
function redirectTo(location: string): NextResponse {
|
||||
return new NextResponse(null, { status: 307, headers: { location } });
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const email = process.env.LOCAL_AUTOLOGIN_EMAIL;
|
||||
const password = process.env.LOCAL_AUTOLOGIN_PASSWORD;
|
||||
if (authMode() === "clerk" || !email || !password) {
|
||||
return new NextResponse(null, { status: 404 });
|
||||
}
|
||||
|
||||
const upstream = await fetch(`${apiOrigin()}/api/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
if (!upstream.ok) {
|
||||
// Land on the real login form rather than a redirect loop back through the
|
||||
// middleware — a wrong password here must be visibly a login problem.
|
||||
return redirectTo("/login?autologin=failed");
|
||||
}
|
||||
const { token } = (await upstream.json()) as { token: string };
|
||||
|
||||
// `next` is same-origin-checked: an open redirect here would be a way to
|
||||
// bounce a freshly-minted session cookie off this host.
|
||||
const requested = request.nextUrl.searchParams.get("next") ?? "/";
|
||||
const target = requested.startsWith("/") && !requested.startsWith("//") ? requested : "/";
|
||||
|
||||
const response = redirectTo(target);
|
||||
response.cookies.set({
|
||||
name: TOKEN_COOKIE,
|
||||
value: token,
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
// Mirrors /auth/session. Behind `tailscale serve` the browser hop is HTTPS,
|
||||
// so a Secure cookie is correct there; over plain http://localhost it would
|
||||
// never be sent back, hence keying on the forwarded scheme.
|
||||
secure: request.headers.get("x-forwarded-proto") === "https",
|
||||
path: "/",
|
||||
maxAge: SESSION_MAX_AGE_SECONDS,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
Reference in New Issue
Block a user