diff --git a/.changeset/vercel-basic-auth-callbacks.md b/.changeset/vercel-basic-auth-callbacks.md new file mode 100644 index 0000000..9a15e03 --- /dev/null +++ b/.changeset/vercel-basic-auth-callbacks.md @@ -0,0 +1,5 @@ +--- +"@plainbrew/vercel-basic-auth": minor +--- + +feat: add `beforeAuth` and `afterAuth` callback options to `basicAuth` diff --git a/packages/vercel-basic-auth/src/index.test.ts b/packages/vercel-basic-auth/src/index.test.ts index 5e3e6d5..bf49702 100644 --- a/packages/vercel-basic-auth/src/index.test.ts +++ b/packages/vercel-basic-auth/src/index.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, test } from "vitest"; +import { afterEach, describe, expect, test, vi } from "vitest"; import { basicAuth } from "./index"; @@ -194,3 +194,67 @@ describe("認証ヘッダーのバリデーション", () => { expect(res?.status).toBe(401); }); }); + +describe("beforeAuth コールバック", () => { + test("beforeAuth が false を返すとき 401 を返す", () => { + const req = makeRequest(makeBasicAuthHeader(USERNAME, PASSWORD)); + const res = basicAuth(req, { + username: USERNAME, + password: PASSWORD, + beforeAuth: () => false, + }); + expect(res?.status).toBe(401); + }); + + test("beforeAuth が true を返すとき通常の認証フローに進む", () => { + const req = makeRequest(makeBasicAuthHeader(USERNAME, PASSWORD)); + expect( + basicAuth(req, { username: USERNAME, password: PASSWORD, beforeAuth: () => true }), + ).toBeNull(); + }); + + test("beforeAuth が false を返すとき認証ヘッダーなしでも 401 を返す", () => { + const req = makeRequest(); + const res = basicAuth(req, { + username: USERNAME, + password: PASSWORD, + beforeAuth: () => false, + }); + expect(res?.status).toBe(401); + }); + + test("NODE_ENV=development (dev=false) のときも beforeAuth は呼ばれる", () => { + process.env.NODE_ENV = "development"; + const beforeAuth = vi.fn(() => false); + const req = makeRequest(); + const res = basicAuth(req, { username: USERNAME, password: PASSWORD, beforeAuth }); + expect(beforeAuth).toHaveBeenCalled(); + expect(res?.status).toBe(401); + }); +}); + +describe("afterAuth コールバック", () => { + test("afterAuth が false を返すとき 401 を返す", () => { + const req = makeRequest(makeBasicAuthHeader(USERNAME, PASSWORD)); + const res = basicAuth(req, { + username: USERNAME, + password: PASSWORD, + afterAuth: () => false, + }); + expect(res?.status).toBe(401); + }); + + test("afterAuth が true を返すとき null を返す", () => { + const req = makeRequest(makeBasicAuthHeader(USERNAME, PASSWORD)); + expect( + basicAuth(req, { username: USERNAME, password: PASSWORD, afterAuth: () => true }), + ).toBeNull(); + }); + + test("認証失敗のとき afterAuth は呼ばれない", () => { + const afterAuth = vi.fn(() => true); + const req = makeRequest(makeBasicAuthHeader(USERNAME, "wrong")); + basicAuth(req, { username: USERNAME, password: PASSWORD, afterAuth }); + expect(afterAuth).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/vercel-basic-auth/src/index.ts b/packages/vercel-basic-auth/src/index.ts index 1d26aff..d025369 100644 --- a/packages/vercel-basic-auth/src/index.ts +++ b/packages/vercel-basic-auth/src/index.ts @@ -13,6 +13,18 @@ export type BasicAuthOptions = { * @default false */ dev?: boolean; + /** + * 組み込みの環境判定後、認証チェック前に呼ばれるコールバック + * - true を返すと通常の認証フローに進む + * - false を返すと認証失敗 (401) を返す + */ + beforeAuth?: (request: Request) => boolean; + /** + * 認証成功後に呼ばれるコールバック + * - true を返すと通過する + * - false を返すと認証失敗 (401) を返す + */ + afterAuth?: (request: Request) => boolean; }; export function basicAuth( @@ -22,6 +34,8 @@ export function basicAuth( password: authPassword, vercelEnvTarget = "only-production", dev = false, + beforeAuth, + afterAuth, }: BasicAuthOptions, ): Response | null { function unauthorized() { @@ -33,6 +47,10 @@ export function basicAuth( }); } + if (beforeAuth && !beforeAuth(request)) { + return unauthorized(); + } + if (process.env.NODE_ENV === "development") { if (!dev) { return null; @@ -79,5 +97,9 @@ export function basicAuth( return unauthorized(); } + if (afterAuth && !afterAuth(request)) { + return unauthorized(); + } + return null; }