mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
refactor(auth): move the request guards out of api-handler into lib/auth/guards
api-handler.ts held two modules: the route wrapper and thirteen guards. The guards now live in lib/auth/guards/, one module per family (the caller, the acting-account carrier, the record modes, the cookie-only helpers, the fresh-factor and recent-proof gates, the Bearer elevation, MFA management). api-handler.ts keeps the wrapper and re-exports every guard, so it stays the one public entry and no route changes its import. The bodies move verbatim; only the version prefixes of moved comments are dropped. The structural guards that read the auth layer now find the module that declares a guard by name (helpers/auth-guards.ts) instead of a fixed path, and a new guard holds the entry: nothing outside the folder but api-handler.ts imports a guard module.
This commit is contained in:
1 parent
9de64cae3e
commit
ee1561496b
40 files changed
+2153
-1668
No files matched your search
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Structural guard: the three read routes that used to write must stay reads.
|
||||
*
|
||||
* `src/lib/api-handler.ts` states the invariant next to `READ_HTTP_METHODS` —
|
||||
* `src/lib/auth/guards/context.ts` states the invariant next to `READ_HTTP_METHODS` —
|
||||
* an MCP-audience token is admitted on GET/HEAD/OPTIONS on the REST surface
|
||||
* because those methods are assumed side-effect-free, and adding a
|
||||
* side-effecting GET silently widens that token's reach. These three routes
|
||||
|
||||
@@ -11,6 +11,8 @@ import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { guardSourceOf } from "@/__tests__/helpers/auth-guards";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
const read = (rel: string) => readFileSync(join(SRC, rel), "utf8");
|
||||
|
||||
@@ -94,18 +96,15 @@ describe("T11/T13 — cookie-only boundaries and no bundle-in-browser", () => {
|
||||
});
|
||||
|
||||
it("requireAdmin and requireFreshMfa remain cookie-only (getSession)", () => {
|
||||
const handler = read("lib/api-handler.ts");
|
||||
// Both resolve via getSession() (cookie-only) — restated as a tripwire so a
|
||||
// future edit that threads a Bearer into either trips this test.
|
||||
const requireAdmin = handler.slice(
|
||||
handler.indexOf("export async function requireAdmin"),
|
||||
handler.indexOf("export async function requireAdmin") + 400,
|
||||
);
|
||||
expect(requireAdmin).toMatch(/getSession\(\)/);
|
||||
const requireFresh = handler.slice(
|
||||
handler.indexOf("export async function requireFreshMfa"),
|
||||
handler.indexOf("export async function requireFreshMfa") + 400,
|
||||
);
|
||||
expect(requireFresh).toMatch(/getSession\(\)/);
|
||||
// future edit that threads a Bearer into either trips this test. Read from
|
||||
// the guard module that declares each, which throws if it is gone.
|
||||
const opening = (name: string) => {
|
||||
const src = guardSourceOf(name, [`export async function ${name}(`]);
|
||||
const start = src.indexOf(`export async function ${name}(`);
|
||||
return src.slice(start, start + 600);
|
||||
};
|
||||
expect(opening("requireAdmin")).toMatch(/getSession\(\)/);
|
||||
expect(opening("requireFreshMfa")).toMatch(/getSession\(\)/);
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
* `GET /api/gamification/achievements` is a read.
|
||||
*
|
||||
* It used to INSERT a `UserAchievement` row for every unlock it noticed,
|
||||
* which is the side-effecting GET the MCP-audience note in `api-handler.ts`
|
||||
* which is the side-effecting GET the MCP-audience note in `lib/auth/guards/context.ts`
|
||||
* warns against. The evaluation still runs here; only the persistence moved
|
||||
* to the `achievement-unlock-sweep` job.
|
||||
*
|
||||
|
||||
@@ -97,7 +97,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
);
|
||||
|
||||
// v1.35.3 — the unlock rows are NOT written here. A GET must not have side
|
||||
// effects (`api-handler.ts`, the MCP-audience note above
|
||||
// effects (`lib/auth/guards/context.ts`, the MCP-audience note above
|
||||
// `READ_HTTP_METHODS`), and this INSERT was one. The badge grid does not
|
||||
// need the rows: `unlocked` comes from the live metrics and the completion
|
||||
// date is derived from the account's own history, so the payload below is
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* `GET /api/sync/state` is a read.
|
||||
*
|
||||
* It used to bump `User.lastSyncedAt` on every call, which is the exact
|
||||
* side-effecting GET the MCP-audience note in `api-handler.ts` warns against,
|
||||
* side-effecting GET the MCP-audience note in `lib/auth/guards/context.ts` warns against,
|
||||
* and it advanced the checkpoint past a window the client had not drained.
|
||||
* The checkpoint is now owned by the ingest boundaries; this route reports it.
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user