diff --git a/web/.env.example b/web/.env.example index 9d0ba74..23955c8 100644 --- a/web/.env.example +++ b/web/.env.example @@ -1,14 +1,5 @@ -# MongoDB Atlas connection string. Never leaves the server. -MONGODB_URI=mongodb+srv://user:password@cluster.mongodb.net/?retryWrites=true&w=majority -MONGODB_DATABASE=fatihsmg4 - -# Write-only token the car sends as `Authorization: Bearer ...` on the PUT endpoints. -# 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= +MONGODB_URI mongodb+srv://boklukunefe:9rprDcfHTjBYza7z@cluster0.1aarkvo.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0 +MONGODB_DATABASE fatihsmg4 +INGEST_TOKEN 066da6fc58a0fcd8a35deccb29e43ef7a0b3bcda5a3c2b99db86e64a5b982ec6 +SESSION_SECRET a89b011dfe7bec22c4f44f89e8e0ef647e6ff3289e84ca1b5b68c413ff7f4b77 +DASHBOARD_PASSWORD \ No newline at end of file diff --git a/web/src/app/api/session/route.ts b/web/src/app/api/session/route.ts index 80f9df4..9cbbcda 100644 --- a/web/src/app/api/session/route.ts +++ b/web/src/app/api/session/route.ts @@ -1,6 +1,5 @@ import { NextResponse } from "next/server"; -import { cookies } from "next/headers"; -import { SESSION_COOKIE, checkDashboardPassword, issueSessionValue, sessionCookieOptions } from "@/lib/auth"; +import { SESSION_COOKIE, checkDashboardPassword, issueSessionValue, sessionCookieOptions } from "@/lib/session"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -17,7 +16,9 @@ export async function POST(request: Request) { await new Promise((resolve) => setTimeout(resolve, 250)); return NextResponse.redirect(new URL("/login?error=1", request.url), { status: 303 }); } - const store = await cookies(); - store.set(SESSION_COOKIE, issueSessionValue(), sessionCookieOptions); - return NextResponse.redirect(new URL("/", request.url), { status: 303 }); + const response = NextResponse.redirect(new URL("/", request.url), { status: 303 }); + // Set on the response rather than through the cookies() store: a freshly constructed + // redirect does not reliably carry what that store recorded. + response.cookies.set(SESSION_COOKIE, issueSessionValue(), sessionCookieOptions); + return response; } diff --git a/web/src/app/globals.css b/web/src/app/globals.css index 1391344..ef58ada 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -353,6 +353,7 @@ a { } .signout { + font: inherit; font-size: 12px; color: var(--muted); background: none; diff --git a/web/src/app/logout/route.ts b/web/src/app/logout/route.ts index 0418232..8b016f6 100644 --- a/web/src/app/logout/route.ts +++ b/web/src/app/logout/route.ts @@ -1,12 +1,22 @@ import { NextResponse } from "next/server"; -import { cookies } from "next/headers"; -import { SESSION_COOKIE } from "@/lib/auth"; +import { SESSION_COOKIE } from "@/lib/session"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; -export async function GET(request: Request) { - const store = await cookies(); - store.delete(SESSION_COOKIE); - return NextResponse.redirect(new URL("/login", request.url)); +/** + * POST only, deliberately. + * + * This used to answer GET behind a ``, 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; } diff --git a/web/src/components/shell.tsx b/web/src/components/shell.tsx index 2467176..a9320aa 100644 --- a/web/src/components/shell.tsx +++ b/web/src/components/shell.tsx @@ -34,9 +34,17 @@ export function Shell({

{title}

{subtitle &&

{subtitle}

} - - Çıkış - + {/* + 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. + */} +
+ +
{children}