From 0695c5844f7e28e46dc3a6087b981e69ed4e41ba Mon Sep 17 00:00:00 2001 From: William Bouchard <46496014+willbouch@users.noreply.github.com> Date: Thu, 18 Sep 2025 13:05:54 -0400 Subject: [PATCH] fix(auth-emailpass): better handle identity with same email error (#13537) * fix(auth-emailpass): better handle identity with same email error * add test * Create blue-laws-argue.md * check for empty object * trueeee * nit * flip condition --- .changeset/blue-laws-argue.md | 5 ++ .../__tests__/services.spec.ts | 75 ++++++++++++++++++- .../auth-emailpass/src/services/emailpass.ts | 73 +++++++++++------- 3 files changed, 126 insertions(+), 27 deletions(-) create mode 100644 .changeset/blue-laws-argue.md diff --git a/.changeset/blue-laws-argue.md b/.changeset/blue-laws-argue.md new file mode 100644 index 0000000000..a45109752e --- /dev/null +++ b/.changeset/blue-laws-argue.md @@ -0,0 +1,5 @@ +--- +"@medusajs/auth-emailpass": patch +--- + +fix(auth-emailpass): better handle identity with same email error diff --git a/packages/modules/providers/auth-emailpass/integration-tests/__tests__/services.spec.ts b/packages/modules/providers/auth-emailpass/integration-tests/__tests__/services.spec.ts index 1f8a12e709..3ed68b19dd 100644 --- a/packages/modules/providers/auth-emailpass/integration-tests/__tests__/services.spec.ts +++ b/packages/modules/providers/auth-emailpass/integration-tests/__tests__/services.spec.ts @@ -1,6 +1,7 @@ import { MedusaError } from "@medusajs/framework/utils" import Scrypt from "scrypt-kdf" import { EmailPassAuthService } from "../../src/services/emailpass" + jest.setTimeout(100000) describe("Email password auth provider", () => { @@ -152,11 +153,83 @@ describe("Email password auth provider", () => { ) }) - it("throw if auth identity with email already exists", async () => { + it("updates identity if it exists but doesnt have app_metadata", async () => { const authServiceSpies = { retrieve: jest.fn().mockImplementation(() => { return { success: true } }), + update: jest.fn().mockImplementation(() => { + return { + provider_identities: [ + { + entity_id: "test@admin.com", + provider: "emailpass", + provider_metadata: { + password: "somehash", + }, + }, + ], + } + }), + } + + const resp = await emailpassService.register( + { body: { email: "test@admin.com", password: "test" } }, + authServiceSpies + ) + + expect(authServiceSpies.retrieve).toHaveBeenCalled() + expect(authServiceSpies.update).toHaveBeenCalled() + + expect(resp.authIdentity?.provider_identities?.[0]).toEqual( + expect.objectContaining({ + entity_id: "test@admin.com", + provider_metadata: {}, + }) + ) + }) + + it("updates identity if it exists but app_metadata is empty", async () => { + const authServiceSpies = { + retrieve: jest.fn().mockImplementation(() => { + return { success: true, app_metadata: {} } + }), + update: jest.fn().mockImplementation(() => { + return { + provider_identities: [ + { + entity_id: "test@admin.com", + provider: "emailpass", + provider_metadata: { + password: "somehash", + }, + }, + ], + } + }), + } + + const resp = await emailpassService.register( + { body: { email: "test@admin.com", password: "test" } }, + authServiceSpies + ) + + expect(authServiceSpies.retrieve).toHaveBeenCalled() + expect(authServiceSpies.update).toHaveBeenCalled() + + expect(resp.authIdentity?.provider_identities?.[0]).toEqual( + expect.objectContaining({ + entity_id: "test@admin.com", + provider_metadata: {}, + }) + ) + }) + + it("throw if auth identity with email already exists and has app_metadata", async () => { + const authServiceSpies = { + retrieve: jest.fn().mockImplementation(() => { + return { success: true, app_metadata: {"user_id": "some-id"} } + }), create: jest.fn().mockImplementation(() => { return { provider_identities: [ diff --git a/packages/modules/providers/auth-emailpass/src/services/emailpass.ts b/packages/modules/providers/auth-emailpass/src/services/emailpass.ts index 7819eecf6e..0fd125f216 100644 --- a/packages/modules/providers/auth-emailpass/src/services/emailpass.ts +++ b/packages/modules/providers/auth-emailpass/src/services/emailpass.ts @@ -6,17 +6,20 @@ import { EmailPassAuthProviderOptions, Logger, } from "@medusajs/framework/types" -import { - AbstractAuthModuleProvider, - isString, - MedusaError, -} from "@medusajs/framework/utils" +import { AbstractAuthModuleProvider, isString, MedusaError, } from "@medusajs/framework/utils" import Scrypt from "scrypt-kdf" +import { isPresent } from "@medusajs/utils" type InjectedDependencies = { logger: Logger } +type AuthIdentityParams = { + email: string; + password: string; + authIdentityService: AuthIdentityProviderService +} + interface LocalServiceConfig extends EmailPassAuthProviderOptions {} export class EmailPassAuthService extends AbstractAuthModuleProvider { @@ -79,25 +82,6 @@ export class EmailPassAuthService extends AbstractAuthModuleProvider { } } - protected async createAuthIdentity({ email, password, authIdentityService }) { - const passwordHash = await this.hashPassword(password) - - const createdAuthIdentity = await authIdentityService.create({ - entity_id: email, - provider_metadata: { - password: passwordHash, - }, - }) - - const copy = JSON.parse(JSON.stringify(createdAuthIdentity)) - const providerIdentity = copy.provider_identities?.find( - (pi) => pi.provider === this.provider - )! - delete providerIdentity.provider_metadata?.password - - return copy - } - async authenticate( userData: AuthenticationInput, authIdentityService: AuthIdentityProviderService @@ -185,17 +169,31 @@ export class EmailPassAuthService extends AbstractAuthModuleProvider { } try { - await authIdentityService.retrieve({ + const identity = await authIdentityService.retrieve({ entity_id: email, }) + // If app_metadata is not defined or empty, it means no actor was assigned to the auth_identity yet (still "claimable") + if (!isPresent(identity.app_metadata)) { + const updatedAuthIdentity = await this.upsertAuthIdentity('update', { + email, + password, + authIdentityService, + }) + + return { + success: true, + authIdentity: updatedAuthIdentity, + } + } + return { success: false, error: "Identity with email already exists", } } catch (error) { if (error.type === MedusaError.Types.NOT_FOUND) { - const createdAuthIdentity = await this.createAuthIdentity({ + const createdAuthIdentity = await this.upsertAuthIdentity('create', { email, password, authIdentityService, @@ -210,4 +208,27 @@ export class EmailPassAuthService extends AbstractAuthModuleProvider { return { success: false, error: error.message } } } + + private async upsertAuthIdentity(type: 'update' | 'create', { email, password, authIdentityService }: AuthIdentityParams) { + const passwordHash = await this.hashPassword(password) + + const authIdentity = type === 'create' ? await authIdentityService.create({ + entity_id: email, + provider_metadata: { + password: passwordHash, + }, + }) : await authIdentityService.update(email, { + provider_metadata: { + password: passwordHash, + }, + }) + + const copy = JSON.parse(JSON.stringify(authIdentity)) + const providerIdentity = copy.provider_identities?.find( + (pi) => pi.provider === this.provider + )! + delete providerIdentity.provider_metadata?.password + + return copy + } }