diff --git a/.changeset/some-corners-relate.md b/.changeset/some-corners-relate.md new file mode 100644 index 0000000..975ec52 --- /dev/null +++ b/.changeset/some-corners-relate.md @@ -0,0 +1,5 @@ +--- +'better-auth-harmony': minor +--- + +Support mapped schema field (#31 thanks @kdcokenny) diff --git a/packages/plugins/src/email/email-schema.test.ts b/packages/plugins/src/email/email-schema.test.ts new file mode 100644 index 0000000..49ef0e3 --- /dev/null +++ b/packages/plugins/src/email/email-schema.test.ts @@ -0,0 +1,113 @@ +import { afterAll, describe, expect, it, vi } from 'vitest'; +// eslint-disable-next-line import/no-relative-packages -- couldn't find a better way to include it +import { getTestInstance } from '../../../../better-auth/packages/better-auth/src/test-utils/test-instance'; +import emailHarmony, { type UserWithNormalizedEmail } from '.'; +import { allEmail, allEmailSignIn, emailForget, emailSignUp } from './matchers'; +// eslint-disable-next-line import/no-relative-packages -- couldn't find a better way to include it +import type { BetterAuthPlugin } from '../../../../better-auth/packages/better-auth/src/types'; + +interface SQLiteDB { + close: () => Promise; +} + +describe('Mapped schema', async () => { + const mockSendEmail = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- false positive + let token = ''; + + const { client, db, auth } = await getTestInstance( + { + emailAndPassword: { + enabled: true, + async sendResetPassword({ url }) { + token = url.split('?')[0]?.split('/').pop() ?? ''; + await mockSendEmail(); + } + }, + plugins: [ + emailHarmony({ + allowNormalizedSignin: true, + matchers: { + signIn: [emailForget, allEmailSignIn, emailSignUp], + validation: [emailForget, allEmail] + }, + schema: { + user: { + fields: { + normalizedEmail: 'normalized_email' + } + } + } + }) as BetterAuthPlugin + ] + }, + { + disableTestUser: true + } + ); + + afterAll(async () => { + // TODO: Open PR for better-auth/src/test-utils/test-instance + await (auth.options.database as unknown as SQLiteDB).close(); + }); + + describe('signup', () => { + it('should normalize email', async () => { + const rawEmail = 'new.email+test@googlemail.com'; + await client.signUp.email({ + email: rawEmail, + password: 'new-password', + name: 'new-name' + }); + const userYes = await db.findOne({ + model: 'user', + where: [ + { + field: 'email', + value: rawEmail + } + ] + }); + // expect(userNo?.email).toBeUndefined(); + expect(userYes?.email).toBe(rawEmail); + }); + + it('should reject temporary emails', async () => { + const rawEmail = 'email@mailinator.com'; + const { error } = await client.signUp.email({ + email: rawEmail, + password: 'new-password', + name: 'new-name' + }); + expect(error).not.toBeNull(); + }); + + it('should prevent signups with email variations', async () => { + const rawEmail = 'test.mail+test1@googlemail.com'; + await client.signUp.email({ + email: rawEmail, + password: 'new-password', + name: 'new-name' + }); + const user = await db.findOne({ + model: 'user', + where: [ + { + field: 'normalizedEmail', + value: 'testmail@gmail.com' + } + ] + }); + expect(user?.email).toBe(rawEmail); + + // Duplicate signup attempt + const { error } = await client.signUp.email({ + email: 'testmail+test2@googlemail.com', + password: 'new-password', + name: 'new-name' + }); + + expect(error?.status).toBe(422); + }); + }); +}); diff --git a/packages/plugins/src/email/index.ts b/packages/plugins/src/email/index.ts index 0eb2010..87ef03a 100644 --- a/packages/plugins/src/email/index.ts +++ b/packages/plugins/src/email/index.ts @@ -11,6 +11,15 @@ export interface UserWithNormalizedEmail extends User { normalizedEmail?: string | null; } +export interface EmailHarmonySchema { + user: { + fields: { + /** Map the normalizedEmail field name to a custom value */ + normalizedEmail?: string; + }; + }; +} + export interface EmailHarmonyOptions { /** * Allow logging in with any version of the unnormalized email address. Also works for password @@ -57,6 +66,8 @@ export interface EmailHarmonyOptions { */ validation?: Matcher[]; }; + /** Pass the `schema` option to customize the field names */ + schema?: EmailHarmonySchema; } interface Context { @@ -84,6 +95,7 @@ const emailHarmony = ({ allowNormalizedSignin = false, validator = validateEmail, matchers = {}, + schema, normalizer = normalizeEmail }: EmailHarmonyOptions = {}): BetterAuthPlugin => ({ @@ -126,6 +138,7 @@ const emailHarmony = ({ fields: { normalizedEmail: { type: 'string', + fieldName: schema?.user.fields.normalizedEmail ?? 'normalizedEmail', required: false, unique: true, input: false,