diff --git a/.changeset/cuddly-bugs-hear.md b/.changeset/cuddly-bugs-hear.md new file mode 100644 index 0000000..fbb9b1b --- /dev/null +++ b/.changeset/cuddly-bugs-hear.md @@ -0,0 +1,6 @@ +--- +"@emdash-cms/auth": patch +"emdash": patch +--- + +Fix auth links and OAuth callbacks to use `/_emdash/api/auth/...` so emailed sign-in, signup, and invite URLs resolve correctly in EmDash. diff --git a/packages/auth/src/invite.ts b/packages/auth/src/invite.ts index d9df84f..0d34692 100644 --- a/packages/auth/src/invite.ts +++ b/packages/auth/src/invite.ts @@ -68,7 +68,7 @@ export async function createInviteToken( }); // Build invite URL - const url = new URL("/api/auth/invite/accept", config.baseUrl); + const url = new URL("/_emdash/api/auth/invite/accept", config.baseUrl); url.searchParams.set("token", token); return { url: url.toString(), email }; diff --git a/packages/auth/src/magic-link/index.ts b/packages/auth/src/magic-link/index.ts index eaa6ef8..f618ad7 100644 --- a/packages/auth/src/magic-link/index.ts +++ b/packages/auth/src/magic-link/index.ts @@ -63,7 +63,7 @@ export async function sendMagicLink( }); // Build magic link URL - const url = new URL("/api/auth/magic-link/verify", config.baseUrl); + const url = new URL("/_emdash/api/auth/magic-link/verify", config.baseUrl); url.searchParams.set("token", token); // Send email diff --git a/packages/auth/src/oauth/consumer.ts b/packages/auth/src/oauth/consumer.ts index 8c29bc7..910e3a3 100644 --- a/packages/auth/src/oauth/consumer.ts +++ b/packages/auth/src/oauth/consumer.ts @@ -40,7 +40,10 @@ export async function createAuthorizationUrl( const provider = getProvider(providerName); const state = generateState(); - const redirectUri = `${config.baseUrl}/api/auth/oauth/${providerName}/callback`; + const redirectUri = new URL( + `/_emdash/api/auth/oauth/${providerName}/callback`, + config.baseUrl, + ).toString(); // Generate PKCE code verifier for providers that support it const codeVerifier = generateCodeVerifier(); diff --git a/packages/auth/src/signup.ts b/packages/auth/src/signup.ts index f61fc3f..fea7dd2 100644 --- a/packages/auth/src/signup.ts +++ b/packages/auth/src/signup.ts @@ -91,7 +91,7 @@ export async function requestSignup( }); // Build verification URL - const url = new URL("/api/auth/signup/verify", config.baseUrl); + const url = new URL("/_emdash/api/auth/signup/verify", config.baseUrl); url.searchParams.set("token", token); // Send email diff --git a/packages/core/tests/unit/auth/invite.test.ts b/packages/core/tests/unit/auth/invite.test.ts index 745587f..3fac637 100644 --- a/packages/core/tests/unit/auth/invite.test.ts +++ b/packages/core/tests/unit/auth/invite.test.ts @@ -56,6 +56,7 @@ describe("Invite", () => { expect(result.email).toBe("new@example.com"); expect(result.url).toContain("https://example.com"); + expect(result.url).toContain("/_emdash/api/auth/invite/accept?token="); expect(result.url).toMatch(TOKEN_PARAM_REGEX); // Should NOT have a token field on the result expect("token" in result).toBe(false); diff --git a/packages/core/tests/unit/auth/magic-link.test.ts b/packages/core/tests/unit/auth/magic-link.test.ts new file mode 100644 index 0000000..1dd073c --- /dev/null +++ b/packages/core/tests/unit/auth/magic-link.test.ts @@ -0,0 +1,53 @@ +import type { AuthAdapter, EmailSendFn } from "@emdash-cms/auth"; +import type { EmailMessage } from "@emdash-cms/auth"; +import { Role, sendMagicLink } from "@emdash-cms/auth"; +import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely"; +import type { Kysely } from "kysely"; +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; + +import type { Database } from "../../../src/database/types.js"; +import { setupTestDatabase, teardownTestDatabase } from "../../utils/test-db.js"; + +describe("Magic Link", () => { + let db: Kysely; + let adapter: AuthAdapter; + let mockEmailSend: EmailSendFn & ReturnType; + let sentEmails: Array; + + beforeEach(async () => { + db = await setupTestDatabase(); + adapter = createKyselyAdapter(db); + sentEmails = []; + mockEmailSend = vi.fn(async (email: EmailMessage) => { + sentEmails.push(email); + }); + }); + + afterEach(async () => { + await teardownTestDatabase(db); + }); + + it("sends verify links through the injected EmDash auth route", async () => { + await adapter.createUser({ + email: "author@example.com", + name: "Author", + role: Role.AUTHOR, + emailVerified: true, + }); + + await sendMagicLink( + { + baseUrl: "https://example.com", + siteName: "Test Site", + email: mockEmailSend, + }, + adapter, + "author@example.com", + ); + + expect(mockEmailSend).toHaveBeenCalledOnce(); + expect(sentEmails[0]!.text).toContain( + "https://example.com/_emdash/api/auth/magic-link/verify?token=", + ); + }); +}); diff --git a/packages/core/tests/unit/auth/signup.test.ts b/packages/core/tests/unit/auth/signup.test.ts index 4c4830e..c4d9e2e 100644 --- a/packages/core/tests/unit/auth/signup.test.ts +++ b/packages/core/tests/unit/auth/signup.test.ts @@ -111,6 +111,9 @@ describe("Self-Signup", () => { expect(mockEmailSend).toHaveBeenCalledTimes(1); expect(sentEmails[0]!.to).toBe("newuser@allowed.com"); expect(sentEmails[0]!.subject).toContain("Test Site"); + expect(sentEmails[0]!.text).toContain( + "https://example.com/_emdash/api/auth/signup/verify?token=", + ); expect(sentEmails[0]!.text).toContain("verify"); });