diff --git a/docs/content/docs/plugins/magic-link.mdx b/docs/content/docs/plugins/magic-link.mdx index 16b2146aa5..7b1a8146a9 100644 --- a/docs/content/docs/plugins/magic-link.mdx +++ b/docs/content/docs/plugins/magic-link.mdx @@ -20,7 +20,7 @@ Magic link or email link is a way to authenticate users without a password. When export const auth = betterAuth({ plugins: [ magicLink({ // [!code highlight] - sendMagicLink: async ({ email, token, url }, ctx) => { // [!code highlight] + sendMagicLink: async ({ email, token, url, metadata }, ctx) => { // [!code highlight] // send email to user // [!code highlight] } // [!code highlight] }) // [!code highlight] @@ -85,6 +85,10 @@ type signInMagicLink = { * redirected to the callbackURL with an `error` query parameter. */ errorCallbackURL?: string = "/error" + /** + * Additional metadata forwarded to the sendMagicLink callback. + */ + metadata?: Record = { inviteId: "123" } } ``` @@ -130,6 +134,7 @@ type magicLinkVerify = { - `email`: The email address of the user. - `url`: The URL to be sent to the user. This URL contains the token. - `token`: The token if you want to send the token with custom URL. +- `metadata`: Additional request metadata passed from `signIn.magicLink`. and a `ctx` context object as the second parameter. diff --git a/packages/better-auth/src/client/client.test.ts b/packages/better-auth/src/client/client.test.ts index e18ebe6b5b..2c42a8aec7 100644 --- a/packages/better-auth/src/client/client.test.ts +++ b/packages/better-auth/src/client/client.test.ts @@ -12,6 +12,7 @@ import { deviceAuthorizationClient, emailOTPClient, genericOAuthClient, + magicLinkClient, multiSessionClient, oidcClient, organizationClient, @@ -270,6 +271,36 @@ describe("type", () => { expectTypeOf(client.test.signOut).toEqualTypeOf<() => Promise>(); }); + it("should infer magic link metadata in sign-in request", () => { + const client = createReactClient({ + plugins: [magicLinkClient()], + }); + + type SignInMagicLinkInput = NonNullable< + Parameters[0] + >; + + expectTypeOf().toMatchTypeOf<{ + email: string; + name?: string | undefined; + callbackURL?: string | undefined; + newUserCallbackURL?: string | undefined; + errorCallbackURL?: string | undefined; + metadata?: Record | undefined; + }>(); + + const request: SignInMagicLinkInput = { + email: "test@email.com", + metadata: { + inviteId: "123", + }, + }; + + expectTypeOf(request.metadata).toEqualTypeOf< + Record | undefined + >(); + }); + it("should infer session", () => { const client = createSolidClient({ plugins: [testClientPlugin(), testClientPlugin2(), twoFactorClient()], diff --git a/packages/better-auth/src/plugins/magic-link/index.ts b/packages/better-auth/src/plugins/magic-link/index.ts index 891f01463b..38e5ae64d5 100644 --- a/packages/better-auth/src/plugins/magic-link/index.ts +++ b/packages/better-auth/src/plugins/magic-link/index.ts @@ -39,6 +39,7 @@ export interface MagicLinkOptions { email: string; url: string; token: string; + metadata?: Record; }, ctx?: GenericEndpointContext | undefined, ) => Awaitable; @@ -112,6 +113,12 @@ const signInMagicLinkBodySchema = z.object({ description: "URL to redirect after error.", }) .optional(), + metadata: z + .record(z.string(), z.any()) + .meta({ + description: "Additional metadata to pass to sendMagicLink.", + }) + .optional(), }); const magicLinkVerifyQuerySchema = z.object({ token: z.string().meta({ @@ -208,7 +215,7 @@ export const magicLink = (options: MagicLinkOptions) => { }, }, async (ctx) => { - const { email } = ctx.body; + const { email, metadata } = ctx.body; const verificationToken = opts?.generateToken ? await opts.generateToken(email) @@ -243,6 +250,7 @@ export const magicLink = (options: MagicLinkOptions) => { email, url: url.toString(), token: verificationToken, + metadata, }, ctx, ); diff --git a/packages/better-auth/src/plugins/magic-link/magic-link.test.ts b/packages/better-auth/src/plugins/magic-link/magic-link.test.ts index b8dfe22104..5d6e3cbb5a 100644 --- a/packages/better-auth/src/plugins/magic-link/magic-link.test.ts +++ b/packages/better-auth/src/plugins/magic-link/magic-link.test.ts @@ -9,6 +9,7 @@ type VerificationEmail = { email: string; token: string; url: string; + metadata?: Record; }; describe("magic link", async () => { @@ -50,6 +51,23 @@ describe("magic link", async () => { "http://localhost:3000/api/auth/magic-link/verify", ), }); + expect(verificationEmail.metadata).toBeUndefined(); + }); + + it("should forward metadata to sendMagicLink", async () => { + await client.signIn.magicLink({ + email: testUser.email, + metadata: { + inviteId: "123", + }, + }); + + expect(verificationEmail).toMatchObject({ + email: testUser.email, + metadata: { + inviteId: "123", + }, + }); }); it("should verify magic link", async () => { const headers = new Headers();