From aa39fda696c30d40ac81e5343ae33f0ccc5b2775 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Thu, 28 May 2026 00:12:17 +0800 Subject: [PATCH] feat: add server-side deckyvault.xyz domain block middleware (Task 3) --- lib/api/__tests__/domain-block-api.test.ts | 114 +++++++++++++++++++++ lib/api/app.ts | 26 +++++ 2 files changed, 140 insertions(+) create mode 100644 lib/api/__tests__/domain-block-api.test.ts diff --git a/lib/api/__tests__/domain-block-api.test.ts b/lib/api/__tests__/domain-block-api.test.ts new file mode 100644 index 0000000..8930c72 --- /dev/null +++ b/lib/api/__tests__/domain-block-api.test.ts @@ -0,0 +1,114 @@ +import { describe, it, expect } from "vitest" +import { Elysia } from "elysia" +import { isDeckyVaultEmail, DOMAIN_BLOCK_ERROR } from "@/lib/auth/domain-block" + +/** + * The domain-block onBeforeHandle handler – mirrors the logic + * wired into the auth group in lib/api/app.ts. Tested in isolation + * here so we don't need to stand up the full app / DB. + */ +const domainBlockOnBeforeHandle = async ({ + request, + set, +}: { + request: Request + set: { status: number } +}) => { + const url = new URL(request.url) + const isSignUp = + url.pathname === "/api/auth/sign-up/email" && + request.method === "POST" + + if (!isSignUp) return + + if (process.env.NODE_ENV !== "development") { + try { + const cloned = request.clone() + const body = await cloned.json() + if (isDeckyVaultEmail(body.email)) { + set.status = 400 + return { error: DOMAIN_BLOCK_ERROR } + } + } catch { + // Malformed body — let Better Auth reject it downstream + } + } +} + +describe("domain block middleware", () => { + it("blocks POST /api/auth/sign-up/email with @deckyvault.xyz email", async () => { + const app = new Elysia() + .onBeforeHandle(domainBlockOnBeforeHandle) + .post("/api/auth/sign-up/email", () => ({ + success: "should not reach this", + })) + + const res = await app.handle( + new Request("http://localhost:3000/api/auth/sign-up/email", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: "bad@deckyvault.xyz", + password: "abcdefghij", + name: "Test", + }), + }), + ) + + expect(res.status).toBe(400) + const body = await res.json() + expect(body.error).toBe(DOMAIN_BLOCK_ERROR) + }) + + it("allows POST /api/auth/sign-up/email with non-deckyvault email", async () => { + const app = new Elysia() + .onBeforeHandle(domainBlockOnBeforeHandle) + .post("/api/auth/sign-up/email", () => ({ + success: true, + })) + + const res = await app.handle( + new Request("http://localhost:3000/api/auth/sign-up/email", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: "good@gmail.com", + password: "abcdefghij", + name: "Test", + }), + }), + ) + + expect(res.status).toBe(200) + }) + + it("ignores non-sign-up routes (GET /api/auth/something)", async () => { + const app = new Elysia() + .onBeforeHandle(domainBlockOnBeforeHandle) + .get("/api/auth/something", () => ({ ok: true })) + + const res = await app.handle( + new Request("http://localhost:3000/api/auth/something"), + ) + + expect(res.status).toBe(200) + const body = await res.json() + expect(body.ok).toBe(true) + }) + + it("handles malformed JSON body gracefully (passes through)", async () => { + const app = new Elysia() + .onBeforeHandle(domainBlockOnBeforeHandle) + .post("/api/auth/sign-up/email", () => ({ success: true })) + + const res = await app.handle( + new Request("http://localhost:3000/api/auth/sign-up/email", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "not-valid-json", + }), + ) + + expect(res.status).toBe(200) + }) +}) \ No newline at end of file diff --git a/lib/api/app.ts b/lib/api/app.ts index f1798d4..b607529 100644 --- a/lib/api/app.ts +++ b/lib/api/app.ts @@ -3,6 +3,7 @@ import { openapi } from "@elysia/openapi" import { cron, Patterns } from "@elysia/cron" import { auth } from "@/lib/auth" import { rateLimit } from "@/lib/auth/rate-limit" +import { isDeckyVaultEmail, DOMAIN_BLOCK_ERROR } from "@/lib/auth/domain-block" import { taskRegistry } from "./cron" import { healthRoutes, @@ -157,6 +158,31 @@ export const app = new Elysia({ prefix: "/api" }) .group("", (app) => app .use(rateLimit("auth")) + .onBeforeHandle(async ({ request, set }) => { + const url = new URL(request.url) + const isSignUp = + url.pathname === "/api/auth/sign-up/email" && + request.method === "POST" + + if (!isSignUp) return + + if (process.env.NODE_ENV !== "development") { + try { + const cloned = request.clone() + const body = await cloned.json() + if (isDeckyVaultEmail(body.email)) { + console.warn( + "[AUTH] Blocked sign-up attempt with deckyvault.xyz email", + body.email, + ) + set.status = 400 + return { error: DOMAIN_BLOCK_ERROR } + } + } catch { + // Malformed body — let Better Auth reject downstream + } + } + }) .use(betterAuth) .use(userRoutes) .use(profilePhotoRoutes)