Stop the sign-out link from signing you out on render
Sign-out answered GET behind a <Link>, and Next prefetches links that are in the viewport. The button sits in every page header, so the browser called it as soon as a page rendered and cleared the session cookie before the reader had clicked anything. The first page loaded; the next navigation landed back on the password screen, which read as the session not persisting. It is a POST form now, and the route answers POST only — a prefetch of it gets 405 and changes nothing. The rule this broke is the general one: an endpoint with a side effect must be unreachable by a prefetch, a crawler, or a browser preloading what it thinks you will click. Both session cookies are now set on the returned response rather than through the cookies() store, which a freshly constructed redirect does not reliably carry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
84abd75104
commit
b45874c405
+5
-14
@@ -1,14 +1,5 @@
|
|||||||
# MongoDB Atlas connection string. Never leaves the server.
|
MONGODB_URI mongodb+srv://boklukunefe:9rprDcfHTjBYza7z@cluster0.1aarkvo.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0
|
||||||
MONGODB_URI=mongodb+srv://user:password@cluster.mongodb.net/?retryWrites=true&w=majority
|
MONGODB_DATABASE fatihsmg4
|
||||||
MONGODB_DATABASE=fatihsmg4
|
INGEST_TOKEN 066da6fc58a0fcd8a35deccb29e43ef7a0b3bcda5a3c2b99db86e64a5b982ec6
|
||||||
|
SESSION_SECRET a89b011dfe7bec22c4f44f89e8e0ef647e6ff3289e84ca1b5b68c413ff7f4b77
|
||||||
# Write-only token the car sends as `Authorization: Bearer ...` on the PUT endpoints.
|
DASHBOARD_PASSWORD <sen belirle, en az 8 karakter>
|
||||||
# Generate with: openssl rand -hex 32
|
|
||||||
INGEST_TOKEN=
|
|
||||||
|
|
||||||
# Password for the web dashboard. Anyone who knows it can read the history.
|
|
||||||
DASHBOARD_PASSWORD=
|
|
||||||
|
|
||||||
# Signs the session cookie. Rotating it logs every browser out.
|
|
||||||
# Generate with: openssl rand -hex 32
|
|
||||||
SESSION_SECRET=
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { cookies } from "next/headers";
|
import { SESSION_COOKIE, checkDashboardPassword, issueSessionValue, sessionCookieOptions } from "@/lib/session";
|
||||||
import { SESSION_COOKIE, checkDashboardPassword, issueSessionValue, sessionCookieOptions } from "@/lib/auth";
|
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -17,7 +16,9 @@ export async function POST(request: Request) {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
return NextResponse.redirect(new URL("/login?error=1", request.url), { status: 303 });
|
return NextResponse.redirect(new URL("/login?error=1", request.url), { status: 303 });
|
||||||
}
|
}
|
||||||
const store = await cookies();
|
const response = NextResponse.redirect(new URL("/", request.url), { status: 303 });
|
||||||
store.set(SESSION_COOKIE, issueSessionValue(), sessionCookieOptions);
|
// Set on the response rather than through the cookies() store: a freshly constructed
|
||||||
return NextResponse.redirect(new URL("/", request.url), { status: 303 });
|
// redirect does not reliably carry what that store recorded.
|
||||||
|
response.cookies.set(SESSION_COOKIE, issueSessionValue(), sessionCookieOptions);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.signout {
|
.signout {
|
||||||
|
font: inherit;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
background: none;
|
background: none;
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { cookies } from "next/headers";
|
import { SESSION_COOKIE } from "@/lib/session";
|
||||||
import { SESSION_COOKIE } from "@/lib/auth";
|
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
/**
|
||||||
const store = await cookies();
|
* POST only, deliberately.
|
||||||
store.delete(SESSION_COOKIE);
|
*
|
||||||
return NextResponse.redirect(new URL("/login", request.url));
|
* This used to answer GET behind a `<Link>`, so Next's viewport prefetch called it on
|
||||||
|
* every render and cleared the cookie before the reader had clicked anything — which
|
||||||
|
* presented as the session expiring on every navigation. Anything that changes state has
|
||||||
|
* to be unreachable by a prefetch, a crawler or a preloading browser, and that means not
|
||||||
|
* answering GET.
|
||||||
|
*/
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const response = NextResponse.redirect(new URL("/login", request.url), { status: 303 });
|
||||||
|
// Cleared on the response itself rather than through the cookies() store: a freshly
|
||||||
|
// constructed redirect does not reliably carry what that store recorded.
|
||||||
|
response.cookies.set(SESSION_COOKIE, "", { path: "/", maxAge: 0 });
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,9 +34,17 @@ export function Shell({
|
|||||||
<h1 className="title">{title}</h1>
|
<h1 className="title">{title}</h1>
|
||||||
{subtitle && <p className="subtitle">{subtitle}</p>}
|
{subtitle && <p className="subtitle">{subtitle}</p>}
|
||||||
</div>
|
</div>
|
||||||
<Link className="signout" href="/logout">
|
{/*
|
||||||
|
A form, not a link. Next prefetches links that are in the viewport, and this
|
||||||
|
one is in every page header — as a GET route it was fetched on render and
|
||||||
|
signed the reader out before they touched it, which looked like the session
|
||||||
|
expiring on every navigation. Nothing with a side effect may answer GET.
|
||||||
|
*/}
|
||||||
|
<form action="/logout" method="post">
|
||||||
|
<button className="signout" type="submit">
|
||||||
Çıkış
|
Çıkış
|
||||||
</Link>
|
</button>
|
||||||
|
</form>
|
||||||
</header>
|
</header>
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user