From ab78a61851f59455b290a78100f7935e5a3c81f7 Mon Sep 17 00:00:00 2001 From: Cesar Alcazar Date: Mon, 19 Oct 2026 17:03:29 -0600 Subject: [PATCH] sdyl405w4: Remove the sign-out feature and the Minnect revoke call --- src/app/api/session/route.ts | 23 ++-------- src/components/App.tsx | 14 +----- src/lib/auth-client.ts | 13 ------ src/lib/minnect.ts | 21 --------- src/lib/session-refresh.ts | 22 +--------- tests/session-refresh.test.ts | 25 ----------- tests/session.test.ts | 82 ++--------------------------------- 7 files changed, 9 insertions(+), 191 deletions(-) diff --git a/src/app/api/session/route.ts b/src/app/api/session/route.ts index 7b101d8..d19fb22 100644 --- a/src/app/api/session/route.ts +++ b/src/app/api/session/route.ts @@ -1,13 +1,14 @@ import { NextResponse, type NextRequest } from "next/server"; import { loginToMinnect, LoginError } from "@/lib/minnect"; import { createSession, getSession } from "@/lib/session-store"; -import { endSession } from "@/lib/session-refresh"; import { SESSION_COOKIE, sessionCookieOptions } from "@/lib/cookie"; // This route holds the session on the server. The browser talks only to this // app; Minnect tokens never leave the server (ADR-001). // -// POST signs in, GET says who the person is, DELETE signs out (revoke + clear). +// POST signs in, GET says who the person is. There is no sign-out: the session ends +// when the server restarts or the cookie is cleared, and Minnect's revoke endpoint +// is never called (the refresh token is shared across the person's devices). // The reusable token-refresh logic lives in session-refresh.ts for the feed // route (a later task) to call before it uses a token. @@ -91,21 +92,3 @@ export async function GET(req: NextRequest) { feedUserId: session.person.feedUserId ?? null, }); } - -/** - * DELETE /api/session - * Sign out: revoke the refresh token at Minnect (best-effort), delete the - * server session, and clear the cookie. Always returns 200: sign-out succeeds - * even when revoke errors. After this, the session is gone, so every - * token-requiring route (and GET /api/session) returns 401 { error: "sign_in" }. - */ -export async function DELETE(req: NextRequest) { - const sessionId = req.cookies.get(SESSION_COOKIE)?.value; - - await endSession(sessionId); - - const res = NextResponse.json({ ok: true }); - // Expire the cookie with the same attributes it was set with. - res.cookies.set(SESSION_COOKIE, "", { ...sessionCookieOptions(), maxAge: 0 }); - return res; -} diff --git a/src/components/App.tsx b/src/components/App.tsx index 2c0a05f..166ef65 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useState } from "react"; -import { loadSession, signOut, type Person } from "@/lib/auth-client"; +import { loadSession, type Person } from "@/lib/auth-client"; import { SignInForm } from "./SignInForm"; import { Logo } from "./Logo"; import { Feed } from "./Feed"; @@ -10,7 +10,6 @@ type State = { kind: "loading" } | { kind: "out" } | { kind: "in"; person: Perso export function App() { const [state, setState] = useState({ kind: "loading" }); - const [signingOut, setSigningOut] = useState(false); useEffect(() => { let live = true; @@ -22,14 +21,6 @@ export function App() { }; }, []); - async function handleSignOut() { - if (signingOut) return; - setSigningOut(true); - await signOut(); - setSigningOut(false); - setState({ kind: "out" }); - } - if (state.kind === "loading") { return (
@@ -54,9 +45,6 @@ export function App() { {state.person.name} {state.person.email} - setState({ kind: "out" })} />
diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts index 5739982..91d0bd5 100644 --- a/src/lib/auth-client.ts +++ b/src/lib/auth-client.ts @@ -31,19 +31,6 @@ export async function loadSession(f: Fetch = fetch): Promise { } } -/** - * DELETE /api/session: sign out. Resolves once the server has cleared the - * session and cookie. A failed request still returns the UI to signed-out, - * because the local session view is no longer trustworthy. - */ -export async function signOut(f: Fetch = fetch): Promise { - try { - await f("/api/session", { method: "DELETE", credentials: "same-origin", cache: "no-store" }); - } catch { - // Ignore: the caller returns to the sign-in screen regardless. - } -} - export type SignInResult = { ok: true; person: Person } | { ok: false; message: string }; /** POST /api/session. A 401 is always the one wrong-credentials message. */ diff --git a/src/lib/minnect.ts b/src/lib/minnect.ts index 0f4f45c..37e1c7f 100644 --- a/src/lib/minnect.ts +++ b/src/lib/minnect.ts @@ -196,27 +196,6 @@ export async function refreshProfileTokens( }; } -/** - * Revoke a profile refresh token on sign-out. - * `POST /api/v4/auth/revoke-token` with `{ refreshToken }`. Best-effort: any - * error (network or non-200) is swallowed so that sign-out still succeeds. - */ -export async function revokeToken( - refreshToken: string, - options: LoginOptions = {}, -): Promise { - const doFetch = options.fetchImpl ?? fetch; - try { - await doFetch(`${PROFILES_BASE_URL}/api/v4/auth/revoke-token`, { - method: "POST", - headers: jsonHeaders(), - body: JSON.stringify({ refreshToken }), - }); - } catch { - // Sign-out still succeeds if revoke fails (spec, "Sign out"). - } -} - /** `GET /api/v1/auth/me` on the feed host. Returns data.id, or null on any failure. */ async function fetchFeedUserId(feedToken: string, doFetch: typeof fetch): Promise { try { diff --git a/src/lib/session-refresh.ts b/src/lib/session-refresh.ts index 7b8d519..461052c 100644 --- a/src/lib/session-refresh.ts +++ b/src/lib/session-refresh.ts @@ -4,12 +4,11 @@ import { updateSession, type SessionData, } from "./session-store"; -import { exchangeFeedToken, refreshProfileTokens, revokeToken } from "./minnect"; +import { exchangeFeedToken, refreshProfileTokens } from "./minnect"; // Reusable server-side session logic the route handlers share. The feed route // (a later task) calls `ensureFreshSession` before using a token, and -// `refreshSession` once after a feed call returns 401. Sign-out uses -// `endSession`. Minnect tokens never leave the server; nothing here is logged. +// `refreshSession` once after a feed call returns 401. Minnect tokens never leave the server; nothing here is logged. /** Refresh when a token expires within this window (spec: two minutes). */ export const REFRESH_WINDOW_MS = 2 * 60 * 1000; @@ -85,20 +84,3 @@ export async function ensureFreshSession( } return session; } - -/** - * Sign-out. Revoke the refresh token (best-effort) and delete the server - * session. Always resolves: a revoke error does not stop sign-out. The caller - * clears the cookie. - */ -export async function endSession( - sessionId: string | undefined, - options: Pick = {}, -): Promise { - if (!sessionId) return; - const session = getSession(sessionId); - if (session) { - await revokeToken(session.profileRefreshToken, options); - } - deleteSession(sessionId); -} diff --git a/tests/session-refresh.test.ts b/tests/session-refresh.test.ts index f7d538c..786f228 100644 --- a/tests/session-refresh.test.ts +++ b/tests/session-refresh.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it, vi } from "vitest"; import { - endSession, ensureFreshSession, refreshSession, } from "@/lib/session-refresh"; @@ -113,27 +112,3 @@ describe("refreshSession failure", () => { expect(getSession(id)).toBeNull(); }); }); - -describe("endSession", () => { - it("revokes the refresh token and deletes the session", async () => { - const id = seedSession(Date.now() + 60 * 60_000); - const fetchImpl = vi.fn(async (_input: RequestInfo | URL) => json({ data: { success: true } })); - - await endSession(id, { fetchImpl: fetchImpl as unknown as typeof fetch }); - - expect(fetchImpl.mock.calls.some(([u]) => String(u).endsWith("/api/v4/auth/revoke-token"))).toBe(true); - expect(getSession(id)).toBeNull(); - }); - - it("still deletes the session when revoke throws", async () => { - const id = seedSession(Date.now() + 60 * 60_000); - const fetchImpl = vi.fn(async (_input: RequestInfo | URL): Promise => { - throw new Error("revoke down"); - }); - - await expect( - endSession(id, { fetchImpl: fetchImpl as unknown as typeof fetch }), - ).resolves.toBeUndefined(); - expect(getSession(id)).toBeNull(); - }); -}); diff --git a/tests/session.test.ts b/tests/session.test.ts index fc341ea..80c0cc4 100644 --- a/tests/session.test.ts +++ b/tests/session.test.ts @@ -1,26 +1,17 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { NextRequest } from "next/server"; -import { DELETE, GET, POST } from "@/app/api/session/route"; +import { GET, POST } from "@/app/api/session/route"; import { SESSION_COOKIE } from "@/lib/cookie"; import { getSession } from "@/lib/session-store"; // A fetch stub that walks the Minnect calls. `overrides` lets a test force a -// non-200 or a weird body at any step. `revoke` covers the sign-out call. +// non-200 or a weird body at any step. function makeFetchStub( - overrides: Partial> = {}, + overrides: Partial> = {}, ) { return vi.fn(async (input: RequestInfo | URL): Promise => { const url = typeof input === "string" ? input : input.toString(); - if (url.endsWith("/api/v4/auth/revoke-token")) { - return ( - overrides.revoke ?? - new Response(JSON.stringify({ data: { success: true } }), { - status: 200, - headers: { "content-type": "application/json" }, - }) - ); - } if (url.endsWith("/api/v4/auth/login")) { return ( overrides.login ?? @@ -193,70 +184,3 @@ describe("GET /api/session", () => { }); }); -function requestWithCookie(method: string, id: string): NextRequest { - return new NextRequest("http://localhost/api/session", { - method, - headers: { cookie: `${SESSION_COOKIE}=${id}` }, - }); -} - -describe("DELETE /api/session", () => { - it("revokes, deletes the session, clears the cookie; next GET is 401", async () => { - const fetchStub = makeFetchStub(); - vi.stubGlobal("fetch", fetchStub); - - const postRes = await POST(postRequest({ email: "person@example.com", password: "right" })); - const id = postRes.cookies.get(SESSION_COOKIE)!.value; - expect(getSession(id)).not.toBeNull(); - - const delRes = await DELETE(requestWithCookie("DELETE", id)); - expect(delRes.status).toBe(200); - - // The revoke-token call was made. - expect( - fetchStub.mock.calls.some(([u]) => String(u).endsWith("/api/v4/auth/revoke-token")), - ).toBe(true); - - // Cookie is cleared (empty value, immediate expiry). - const cookie = delRes.cookies.get(SESSION_COOKIE); - expect(cookie?.value).toBe(""); - expect(cookie?.maxAge).toBe(0); - - // Session is gone. - expect(getSession(id)).toBeNull(); - - // The next token-requiring call (GET) returns 401 sign_in. - const getRes = await GET(requestWithCookie("GET", id)); - expect(getRes.status).toBe(401); - expect(await getRes.json()).toEqual({ error: "sign_in" }); - }); - - it("still succeeds when revoke returns an error", async () => { - vi.stubGlobal("fetch", makeFetchStub()); - const postRes = await POST(postRequest({ email: "person@example.com", password: "right" })); - const id = postRes.cookies.get(SESSION_COOKIE)!.value; - - // Now make revoke fail. - vi.stubGlobal( - "fetch", - makeFetchStub({ - revoke: new Response(JSON.stringify({ data: null }), { - status: 500, - headers: { "content-type": "application/json" }, - }), - }), - ); - - const delRes = await DELETE(requestWithCookie("DELETE", id)); - expect(delRes.status).toBe(200); - expect(delRes.cookies.get(SESSION_COOKIE)?.value).toBe(""); - expect(getSession(id)).toBeNull(); - }); - - it("no cookie: still 200 and clears the cookie", async () => { - vi.stubGlobal("fetch", makeFetchStub()); - const delRes = await DELETE(new NextRequest("http://localhost/api/session", { method: "DELETE" })); - expect(delRes.status).toBe(200); - expect(delRes.cookies.get(SESSION_COOKIE)?.value).toBe(""); - }); -});