diff --git a/integration-tests/modules/__tests__/auth/admin/email-password-provider.spec.ts b/integration-tests/modules/__tests__/auth/admin/email-password-provider.spec.ts index 680e66f62f..8298a49478 100644 --- a/integration-tests/modules/__tests__/auth/admin/email-password-provider.spec.ts +++ b/integration-tests/modules/__tests__/auth/admin/email-password-provider.spec.ts @@ -41,11 +41,15 @@ medusaIntegrationTestRunner({ ) await authService.create({ - provider: "emailpass", - entity_id: email, - provider_metadata: { - password: passwordHash, - }, + provider_identities: [ + { + provider: "emailpass", + entity_id: email, + provider_metadata: { + password: passwordHash, + }, + }, + ], }) const response = await api @@ -72,11 +76,15 @@ medusaIntegrationTestRunner({ ) await authService.create({ - provider: "emailpass", - entity_id: email, - provider_metadata: { - password: passwordHash, - }, + provider_identities: [ + { + provider: "emailpass", + entity_id: email, + provider_metadata: { + password: passwordHash, + }, + }, + ], }) const error = await api diff --git a/packages/core/types/src/auth/common/auth-identity.ts b/packages/core/types/src/auth/common/auth-identity.ts index 579e239eab..522b7204a2 100644 --- a/packages/core/types/src/auth/common/auth-identity.ts +++ b/packages/core/types/src/auth/common/auth-identity.ts @@ -12,30 +12,14 @@ export type AuthIdentityDTO = { id: string /** - * The ID of the provider used to authenticate the user. - */ - provider: string - - /** - * The user's identifier. For example, when using the `emailpass` - * provider, the `entity_id` would be the user's email. - */ - entity_id: string + * The list of provider identities linked to the auth identity. + **/ + provider_identities?: ProviderIdentityDTO[] /** * Holds information related to the actor IDs tied to the auth identity. */ app_metadata?: Record - - /** - * Holds custom data related to the provider in key-value pairs. - */ - provider_metadata?: Record - - /** - * Holds custom data related to the user in key-value pairs. - */ - user_metadata: Record } /** @@ -50,31 +34,14 @@ export type CreateAuthIdentityDTO = { id?: string /** - * The ID of the provider used to authenticate - * the user. - */ - provider: string - - /** - * The user's identifier. For example, when using the `emailpass` - * provider, the `entity_id` would be the user's email. - */ - entity_id: string + * The list of provider identities linked to the auth identity. + **/ + provider_identities?: CreateProviderIdentityDTO[] /** * Holds information related to the actor IDs tied to the auth identity. */ app_metadata?: Record - - /** - * Holds custom data related to the provider in key-value pairs. - */ - provider_metadata?: Record - - /** - * Holds custom data related to the user in key-value pairs. - */ - user_metadata?: Record } /** @@ -92,6 +59,101 @@ export type UpdateAuthIdentityDTO = { * Holds information related to the actor IDs tied to the auth identity. */ app_metadata?: Record +} + +/** + * @interface + * + * The provider identity details. + */ +export type ProviderIdentityDTO = { + /** + * The ID of the provider identity. + */ + id: string + + /* + * The ID of the provider used to authenticate the user. + */ + provider: string + + /** + * The user's identifier. For example, when using the `emailpass` + * provider, the `entity_id` would be the user's email. + */ + entity_id: string + + /** + * The auth identity linked to the provider identity. + */ + auth_identity?: AuthIdentityDTO + + /** + * Holds custom data related to the provider in key-value pairs. + */ + provider_metadata?: Record + + /** + * Holds custom data related to the user in key-value pairs. + */ + user_metadata?: Record +} + +/** + * @interface + * + * The provider identity to be created. + */ +export type CreateProviderIdentityDTO = { + /** + * The ID of the provider identity. + */ + id?: string + + /* + * The ID of the provider used to authenticate the user. + */ + provider: string + + /** + * The user's identifier. For example, when using the `emailpass` + * provider, the `entity_id` would be the user's email. + */ + entity_id: string + + /** + * The auth identity linked to the provider identity. Needs to be specified if creating a new provider identity directly. + */ + auth_identity_id?: string + + /** + * Holds custom data related to the provider in key-value pairs. + */ + provider_metadata?: Record + + /** + * Holds custom data related to the user in key-value pairs. + */ + user_metadata?: Record +} + +/** + * @interface + * + * The provider identity to be created. + */ +export type UpdateProviderIdentityDTO = { + /** + * The ID of the provider identity. + */ + id: string + + /** + * The user's identifier. For example, when using the `emailpass` + * provider, the `entity_id` would be the user's email. + */ + entity_id: string + /** * Holds custom data related to the provider in key-value pairs. */ @@ -114,7 +176,17 @@ export interface FilterableAuthIdentityProps id?: string[] /** - * Filter the auth identities by the ID of their auth provider. + * The provider identities to filter the auth identity by. */ - provider?: string[] | string + provider_identities?: { + /** + * Filter the provider identities by the ID of the provider identity ID they are linked to. + */ + entity_id?: string + + /** + * Filter the provider identities by the provider handle. + */ + provider?: string + } } diff --git a/packages/core/types/src/auth/common/provider.ts b/packages/core/types/src/auth/common/provider.ts index 8300e27d02..87e3b103d3 100644 --- a/packages/core/types/src/auth/common/provider.ts +++ b/packages/core/types/src/auth/common/provider.ts @@ -1,3 +1,5 @@ +import { AuthIdentityDTO } from "./auth-identity" + /** * @interface * @@ -12,7 +14,7 @@ export type AuthenticationResponse = { /** * The authenticated user's details. */ - authIdentity?: any + authIdentity?: AuthIdentityDTO /** * If an error occurs during the authentication process, diff --git a/packages/core/types/src/auth/provider.ts b/packages/core/types/src/auth/provider.ts index b028caa22b..639e4f4dac 100644 --- a/packages/core/types/src/auth/provider.ts +++ b/packages/core/types/src/auth/provider.ts @@ -2,16 +2,17 @@ import { AuthIdentityDTO, AuthenticationInput, AuthenticationResponse, - CreateAuthIdentityDTO, } from "./common" +// This interface currently won't allow for linking multiple providers to a single auth entity. That flow is more complex and not supported yet. export interface AuthIdentityProviderService { // The provider is injected by the auth identity module - retrieve: (selector: { + retrieve: (selector: { entity_id: string }) => Promise + create: (data: { entity_id: string - provider: string + provider_metadata?: Record + user_metadata?: Record }) => Promise - create: (data: CreateAuthIdentityDTO) => Promise } /** diff --git a/packages/core/types/src/auth/service.ts b/packages/core/types/src/auth/service.ts index aca52b21b6..10935a1882 100644 --- a/packages/core/types/src/auth/service.ts +++ b/packages/core/types/src/auth/service.ts @@ -10,13 +10,6 @@ import { Context } from "../shared-context" import { FindConfig } from "../common" import { IModuleService } from "../modules-sdk" -/** - * @ignore - */ -export type JWTGenerationOptions = { - expiresIn?: string | number -} - /** * The main service interface for the Auth Module. */ @@ -196,12 +189,16 @@ export interface IAuthModuleService extends IModuleService { * @example * const authIdentities = await authModuleService.create([ * { - * provider: "emailpass", - * entity_id: "user@example.com", + * provider_identities: [{ + * provider: "emailpass", + * entity_id: "user@example.com", + * }] * }, * { - * provider: "google", - * entity_id: "user@gmail.com", + * provider_identities: [{ + * provider: "google", + * entity_id: "user@gmail.com", + * }] * }, * ]) */ @@ -219,8 +216,10 @@ export interface IAuthModuleService extends IModuleService { * * @example * const authIdentity = await authModuleService.create({ - * provider: "emailpass", - * entity_id: "user@example.com", + * provider_identities: [{ + * provider: "emailpass", + * entity_id: "user@example.com", + * }] * }) */ create( diff --git a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts index 839a1aa3fc..31b5b02cf6 100644 --- a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts +++ b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts @@ -45,7 +45,9 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { await service.validateCallback(auth_provider, authData) const entityIdKey = `${actor_type}_id` - const entityId = authIdentity.app_metadata?.[entityIdKey] + const entityId = authIdentity?.app_metadata?.[entityIdKey] as + | string + | undefined if (success) { const { http } = req.scope.resolve( ContainerRegistrationKeys.CONFIG_MODULE @@ -54,9 +56,9 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { const { jwtSecret, jwtExpiresIn } = http const token = generateJwtToken( { - actor_id: entityId, + actor_id: entityId ?? "", actor_type, - auth_identity_id: authIdentity.id, + auth_identity_id: authIdentity?.id ?? "", app_metadata: { [entityIdKey]: entityId, }, diff --git a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/route.ts b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/route.ts index e3a6caacb4..07c9a61dcb 100644 --- a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/route.ts +++ b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/route.ts @@ -57,14 +57,16 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { ).projectConfig const entityIdKey = `${actor_type}_id` - const entityId = authIdentity.app_metadata?.[entityIdKey] + const entityId = authIdentity?.app_metadata?.[entityIdKey] as + | string + | undefined const { jwtSecret, jwtExpiresIn } = http const token = generateJwtToken( { - actor_id: entityId, + actor_id: entityId ?? "", actor_type, - auth_identity_id: authIdentity.id, + auth_identity_id: authIdentity?.id ?? "", app_metadata: { [entityIdKey]: entityId, }, diff --git a/packages/medusa/src/commands/user.ts b/packages/medusa/src/commands/user.ts index b7fb135ea6..9f1f2eeac3 100644 --- a/packages/medusa/src/commands/user.ts +++ b/packages/medusa/src/commands/user.ts @@ -56,8 +56,9 @@ export default async function ({ process.exit(1) } + // We know the authIdentity is not undefined await authService.update({ - id: authIdentity.id, + id: authIdentity!.id, app_metadata: { user_id: user.id, }, diff --git a/packages/modules/auth/integration-tests/__fixtures__/auth-identity/index.ts b/packages/modules/auth/integration-tests/__fixtures__/auth-identity/index.ts index 543cbd25bd..953188c1ea 100644 --- a/packages/modules/auth/integration-tests/__fixtures__/auth-identity/index.ts +++ b/packages/modules/auth/integration-tests/__fixtures__/auth-identity/index.ts @@ -1,34 +1,36 @@ +import { IAuthModuleService } from "@medusajs/types" import { AuthIdentity } from "@models" -import { SqlEntityManager } from "@mikro-orm/postgresql" export async function createAuthIdentities( - manager: SqlEntityManager, + service: IAuthModuleService, userData: any[] = [ { id: "test-id", - entity_id: "test-id", - provider: "manual", + provider_identities: [ + { + entity_id: "test-id", + provider: "manual", + }, + ], }, { id: "test-id-1", - entity_id: "test-id-1", - provider: "manual", + provider_identities: [ + { + entity_id: "test-id-1", + provider: "manual", + }, + ], }, { - entity_id: "test-id-2", - provider: "store", + provider_identities: [ + { + entity_id: "test-id-2", + provider: "store", + }, + ], }, ] ): Promise { - const authIdentities: AuthIdentity[] = [] - - for (const user of userData) { - const authIdentity = manager.create(AuthIdentity, user) - - authIdentities.push(authIdentity) - } - - await manager.persistAndFlush(authIdentities) - - return authIdentities + return await service.create(userData) } diff --git a/packages/modules/auth/integration-tests/__fixtures__/providers/default-provider.ts b/packages/modules/auth/integration-tests/__fixtures__/providers/default-provider.ts index fc261d2ea5..1f48ef9763 100644 --- a/packages/modules/auth/integration-tests/__fixtures__/providers/default-provider.ts +++ b/packages/modules/auth/integration-tests/__fixtures__/providers/default-provider.ts @@ -23,10 +23,14 @@ export class AuthServiceFixtures extends AbstractAuthModuleProvider { try { authIdentity = await service.retrieve({ entity_id: email, - provider: this.provider, }) - if (authIdentity.provider_metadata?.password === password) { + // The provider has to be present, guaranteed by the retrieve filter above. + const providerIdentity = authIdentity.provider_identities?.find( + (pi) => pi.provider === this.provider + )! + + if (providerIdentity.provider_metadata?.password === password) { return { success: true, authIdentity, @@ -36,7 +40,6 @@ export class AuthServiceFixtures extends AbstractAuthModuleProvider { if (error.type === MedusaError.Types.NOT_FOUND) { const createdAuthIdentity = await service.create({ entity_id: email, - provider: this.provider, provider_metadata: { password, }, diff --git a/packages/modules/auth/integration-tests/__tests__/auth-module-service/auth-identity.spec.ts b/packages/modules/auth/integration-tests/__tests__/auth-module-service/auth-identity.spec.ts index 5379331336..c7b70bb9d2 100644 --- a/packages/modules/auth/integration-tests/__tests__/auth-module-service/auth-identity.spec.ts +++ b/packages/modules/auth/integration-tests/__tests__/auth-module-service/auth-identity.spec.ts @@ -13,22 +13,31 @@ moduleIntegrationTestRunner({ }: SuiteOptions) => { describe("AuthModuleService - AuthIdentity", () => { beforeEach(async () => { - await createAuthIdentities(MikroOrmWrapper.forkManager()) + await createAuthIdentities(service) }) describe("listAuthIdentities", () => { it("should list authIdentities", async () => { - const authIdentities = await service.list() + const authIdentities = await service.list( + {}, + { relations: ["provider_identities"] } + ) expect(authIdentities).toEqual([ expect.objectContaining({ - provider: "store", + provider_identities: [ + expect.objectContaining({ provider: "store" }), + ], }), expect.objectContaining({ - provider: "manual", + provider_identities: [ + expect.objectContaining({ provider: "manual" }), + ], }), expect.objectContaining({ - provider: "manual", + provider_identities: [ + expect.objectContaining({ provider: "manual" }), + ], }), ]) }) @@ -47,7 +56,9 @@ moduleIntegrationTestRunner({ it("should list authIdentities by provider", async () => { const authIdentities = await service.list({ - provider: "manual", + provider_identities: { + provider: "manual", + }, }) expect(authIdentities).toEqual([ @@ -63,25 +74,34 @@ moduleIntegrationTestRunner({ describe("listAndCountAuthIdentities", () => { it("should list and count authIdentities", async () => { - const [authIdentities, count] = await service.listAndCount() + const [authIdentities, count] = await service.listAndCount( + {}, + { relations: ["provider_identities"] } + ) expect(count).toEqual(3) expect(authIdentities).toEqual([ expect.objectContaining({ - provider: "store", + provider_identities: [ + expect.objectContaining({ provider: "store" }), + ], }), expect.objectContaining({ - provider: "manual", + provider_identities: [ + expect.objectContaining({ provider: "manual" }), + ], }), expect.objectContaining({ - provider: "manual", + provider_identities: [ + expect.objectContaining({ provider: "manual" }), + ], }), ]) }) it("should listAndCount authIdentities by provider_id", async () => { const [authIdentities, count] = await service.listAndCount({ - provider: "manual", + provider_identities: { provider: "manual" }, }) expect(count).toEqual(2) @@ -131,7 +151,11 @@ moduleIntegrationTestRunner({ id: "test-id-1", }) ) - expect(authIdentity["password_hash"]).toEqual(undefined) + expect( + authIdentity.provider_identities?.[0].provider_metadata?.[ + "password_hash" + ] + ).toEqual(undefined) }) it("should throw an error when a authIdentityId is not provided", async () => { @@ -196,14 +220,14 @@ moduleIntegrationTestRunner({ await service.update([ { id, - provider_metadata: { email: "test@email.com" }, + app_metadata: { email: "test@email.com" }, }, ]) const [authIdentity] = await service.list({ id: [id] }) expect(authIdentity).toEqual( expect.objectContaining({ - provider_metadata: { email: "test@email.com" }, + app_metadata: { email: "test@email.com" }, }) ) }) @@ -214,8 +238,12 @@ moduleIntegrationTestRunner({ await service.create([ { id: "test", - provider: "manual", - entity_id: "test", + provider_identities: [ + { + provider: "manual", + entity_id: "test", + }, + ], }, ]) diff --git a/packages/modules/auth/integration-tests/__tests__/auth-module-service/index.spec.ts b/packages/modules/auth/integration-tests/__tests__/auth-module-service/index.spec.ts index 1a9ff444fe..2d54159b24 100644 --- a/packages/modules/auth/integration-tests/__tests__/auth-module-service/index.spec.ts +++ b/packages/modules/auth/integration-tests/__tests__/auth-module-service/index.spec.ts @@ -28,11 +28,15 @@ moduleIntegrationTestRunner({ describe("Auth Module Service", () => { beforeEach(async () => { await service.create({ - entity_id: "test@admin.com", - provider: "plaintextpass", - provider_metadata: { - password: "plaintext", - }, + provider_identities: [ + { + entity_id: "test@admin.com", + provider: "plaintextpass", + provider_metadata: { + password: "plaintext", + }, + }, + ], }) }) @@ -65,7 +69,9 @@ moduleIntegrationTestRunner({ success: true, authIdentity: expect.objectContaining({ id: expect.any(String), - entity_id: "test@admin.com", + provider_identities: [ + expect.objectContaining({ entity_id: "test@admin.com" }), + ], }), }) ) @@ -97,12 +103,17 @@ moduleIntegrationTestRunner({ }, }) - const dbAuthIdentity = await service.retrieve(result.authIdentity.id) + const dbAuthIdentity = await service.retrieve( + result.authIdentity?.id!, + { relations: ["provider_identities"] } + ) expect(dbAuthIdentity).toEqual( expect.objectContaining({ id: expect.any(String), - entity_id: "new@admin.com", + provider_identities: [ + expect.objectContaining({ entity_id: "new@admin.com" }), + ], }) ) }) diff --git a/packages/modules/auth/src/migrations/.snapshot-medusa-auth.json b/packages/modules/auth/src/migrations/.snapshot-medusa-auth.json index a8e15bdf4b..9aec33a519 100644 --- a/packages/modules/auth/src/migrations/.snapshot-medusa-auth.json +++ b/packages/modules/auth/src/migrations/.snapshot-medusa-auth.json @@ -1,7 +1,68 @@ { - "namespaces": ["public"], + "namespaces": [ + "public" + ], "name": "public", "tables": [ + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "app_metadata": { + "name": "app_metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + } + }, + "name": "auth_identity", + "schema": "public", + "indexes": [ + { + "keyName": "auth_identity_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {} + }, { "columns": { "id": { @@ -31,6 +92,15 @@ "nullable": false, "mappedType": "text" }, + "auth_identity_id": { + "name": "auth_identity_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, "user_metadata": { "name": "user_metadata", "type": "jsonb", @@ -40,15 +110,6 @@ "nullable": true, "mappedType": "json" }, - "app_metadata": { - "name": "app_metadata", - "type": "jsonb", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "mappedType": "json" - }, "provider_metadata": { "name": "provider_metadata", "type": "jsonb", @@ -57,28 +118,75 @@ "primary": false, "nullable": true, "mappedType": "json" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" } }, - "name": "auth_identity", + "name": "provider_identity", "schema": "public", "indexes": [ { - "keyName": "IDX_auth_identity_provider_entity_id", - "columnNames": ["provider", "entity_id"], - "composite": true, + "keyName": "IDX_provider_identity_auth_identity_id", + "columnNames": [], + "composite": false, "primary": false, - "unique": true + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_provider_identity_auth_identity_id\" ON \"provider_identity\" (auth_identity_id)" }, { - "keyName": "auth_identity_pkey", - "columnNames": ["id"], + "keyName": "IDX_provider_identity_provider_entity_id", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_provider_identity_provider_entity_id\" ON \"provider_identity\" (entity_id, provider)" + }, + { + "keyName": "provider_identity_pkey", + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true } ], "checks": [], - "foreignKeys": {} + "foreignKeys": { + "provider_identity_auth_identity_id_foreign": { + "constraintName": "provider_identity_auth_identity_id_foreign", + "columnNames": [ + "auth_identity_id" + ], + "localTableName": "public.provider_identity", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.auth_identity", + "deleteRule": "cascade", + "updateRule": "cascade" + } + } } ] } diff --git a/packages/modules/auth/src/migrations/Migration20240205025928.ts b/packages/modules/auth/src/migrations/Migration20240205025928.ts index 1590f823ba..fc950356f9 100644 --- a/packages/modules/auth/src/migrations/Migration20240205025928.ts +++ b/packages/modules/auth/src/migrations/Migration20240205025928.ts @@ -5,6 +5,9 @@ export class Migration20240205025928 extends Migration { this.addSql( 'create table if not exists "auth_identity" ("id" text not null, "entity_id" text not null, "provider" text not null, "user_metadata" jsonb null, "app_metadata" jsonb null, "provider_metadata" jsonb null, constraint "auth_identity_pkey" primary key ("id"));' ) + this.addSql( + 'alter table "auth_identity" drop constraint if exists "IDX_auth_identity_provider_entity_id"' + ) this.addSql( 'alter table "auth_identity" add constraint "IDX_auth_identity_provider_entity_id" unique ("provider", "entity_id");' ) diff --git a/packages/modules/auth/src/migrations/Migration20240529080336.ts b/packages/modules/auth/src/migrations/Migration20240529080336.ts new file mode 100644 index 0000000000..0394962a9b --- /dev/null +++ b/packages/modules/auth/src/migrations/Migration20240529080336.ts @@ -0,0 +1,30 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20240529080336 extends Migration { + + async up(): Promise { + this.addSql('create table if not exists "provider_identity" ("id" text not null, "entity_id" text not null, "provider" text not null, "auth_identity_id" text not null, "user_metadata" jsonb null, "provider_metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), constraint "provider_identity_pkey" primary key ("id"));'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_provider_identity_auth_identity_id" ON "provider_identity" (auth_identity_id);'); + this.addSql('CREATE UNIQUE INDEX IF NOT EXISTS "IDX_provider_identity_provider_entity_id" ON "provider_identity" (entity_id, provider);'); + + this.addSql('alter table if exists "provider_identity" add constraint "provider_identity_auth_identity_id_foreign" foreign key ("auth_identity_id") references "auth_identity" ("id") on update cascade on delete cascade;'); + + this.addSql('alter table if exists "auth_identity" add column if not exists "created_at" timestamptz not null default now(), add column "updated_at" timestamptz not null default now();'); + + this.addSql('alter table if exists "auth_identity" drop constraint if exists "IDX_auth_identity_provider_entity_id";'); + this.addSql('alter table if exists "auth_identity" drop column if exists "entity_id";'); + this.addSql('alter table if exists "auth_identity" drop column if exists "provider";'); + this.addSql('alter table if exists "auth_identity" drop column if exists "user_metadata";'); + this.addSql('alter table if exists "auth_identity" drop column if exists "provider_metadata";'); + } + + async down(): Promise { + this.addSql('drop table if exists "provider_identity" cascade;'); + + this.addSql('alter table if exists "auth_identity" add column if not exists "entity_id" text not null, add column "provider" text not null, add column "user_metadata" jsonb null, add column "provider_metadata" jsonb null;'); + this.addSql('alter table if exists "auth_identity" alter column if exists "app_metadata" type jsonb using ("app_metadata"::jsonb);'); + this.addSql('alter table if exists "auth_identity" alter column if exists "app_metadata" set not null;'); + this.addSql('alter table if exists "auth_identity" add constraint "IDX_auth_identity_provider_entity_id" unique ("provider", "entity_id");'); + } + +} diff --git a/packages/modules/auth/src/models/auth-identity.ts b/packages/modules/auth/src/models/auth-identity.ts index 75d3f805fe..3b32192113 100644 --- a/packages/modules/auth/src/models/auth-identity.ts +++ b/packages/modules/auth/src/models/auth-identity.ts @@ -1,50 +1,45 @@ import { BeforeCreate, + Collection, Entity, OnInit, - OptionalProps, + OneToMany, PrimaryKey, Property, - Unique, } from "@mikro-orm/core" import { generateEntityId } from "@medusajs/utils" - -type OptionalFields = "provider_metadata" | "app_metadata" | "user_metadata" +import ProviderIdentity from "./provider-identity" @Entity() -@Unique({ - properties: ["provider", "entity_id"], - name: "IDX_auth_identity_provider_entity_id", -}) export default class AuthIdentity { - [OptionalProps]: OptionalFields - @PrimaryKey({ columnType: "text" }) id!: string - @Property({ columnType: "text" }) - entity_id: string - - @Property({ columnType: "text" }) - provider: string - - @Property({ columnType: "jsonb", nullable: true }) - user_metadata: Record | null + @OneToMany(() => ProviderIdentity, (o) => o.auth_identity) + provider_identities = new Collection(this) @Property({ columnType: "jsonb", nullable: true }) app_metadata: Record | null - @Property({ columnType: "jsonb", nullable: true }) - provider_metadata: Record | null = null + @Property({ + onCreate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date @BeforeCreate() + @OnInit() onCreate() { this.id = generateEntityId(this.id, "authid") } - - @OnInit() - onInit() { - this.id = generateEntityId(this.id, "authid") - } } diff --git a/packages/modules/auth/src/models/index.ts b/packages/modules/auth/src/models/index.ts index f16b9640f7..dd39d3deb5 100644 --- a/packages/modules/auth/src/models/index.ts +++ b/packages/modules/auth/src/models/index.ts @@ -1 +1,2 @@ export { default as AuthIdentity } from "./auth-identity" +export { default as ProviderIdentity } from "./provider-identity" diff --git a/packages/modules/auth/src/models/provider-identity.ts b/packages/modules/auth/src/models/provider-identity.ts new file mode 100644 index 0000000000..bf6385938f --- /dev/null +++ b/packages/modules/auth/src/models/provider-identity.ts @@ -0,0 +1,84 @@ +import { + BeforeCreate, + Entity, + ManyToOne, + OnInit, + PrimaryKey, + Property, +} from "@mikro-orm/core" + +import { + createPsqlIndexStatementHelper, + generateEntityId, +} from "@medusajs/utils" +import AuthIdentity from "./auth-identity" + +const providerEntityIdIndexName = "IDX_provider_identity_provider_entity_id" +const providerEntityIdIndexStatement = createPsqlIndexStatementHelper({ + name: providerEntityIdIndexName, + tableName: "provider_identity", + columns: ["entity_id", "provider"], + unique: true, +}) + +const authIdentityIndexName = "IDX_provider_identity_auth_identity_id" +const authIdentityIndexStatement = createPsqlIndexStatementHelper({ + name: authIdentityIndexName, + tableName: "provider_identity", + columns: ["auth_identity_id"], +}) + +@Entity() +@providerEntityIdIndexStatement.MikroORMIndex() +@authIdentityIndexStatement.MikroORMIndex() +export default class ProviderIdentity { + @PrimaryKey({ columnType: "text" }) + id!: string + + @Property({ columnType: "text" }) + entity_id: string + + @Property({ columnType: "text" }) + provider: string + + @ManyToOne(() => AuthIdentity, { + columnType: "text", + fieldName: "auth_identity_id", + mapToPk: true, + onDelete: "cascade", + }) + auth_identity_id: string + + @ManyToOne(() => AuthIdentity, { + persist: false, + }) + auth_identity: AuthIdentity + + @Property({ columnType: "jsonb", nullable: true }) + user_metadata: Record | null + + @Property({ columnType: "jsonb", nullable: true }) + provider_metadata: Record | null = null + + @Property({ + onCreate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date + + @BeforeCreate() + @OnInit() + onCreate() { + this.id = generateEntityId(this.id, "provid") + this.auth_identity_id ??= this.auth_identity?.id ?? null + } +} diff --git a/packages/modules/auth/src/services/auth-module.ts b/packages/modules/auth/src/services/auth-module.ts index c94264a660..39397a69d0 100644 --- a/packages/modules/auth/src/services/auth-module.ts +++ b/packages/modules/auth/src/services/auth-module.ts @@ -10,7 +10,7 @@ import { ModulesSdkTypes, } from "@medusajs/types" -import { AuthIdentity } from "@models" +import { AuthIdentity, ProviderIdentity } from "@models" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" @@ -21,34 +21,40 @@ import { ModulesSdkUtils, } from "@medusajs/utils" import AuthProviderService from "./auth-provider" +import { populate } from "dotenv" type InjectedDependencies = { baseRepository: DAL.RepositoryService authIdentityService: ModulesSdkTypes.InternalModuleService + providerIdentityService: ModulesSdkTypes.InternalModuleService authProviderService: AuthProviderService } -const generateMethodForModels = [AuthIdentity] +const generateMethodForModels = [AuthIdentity, ProviderIdentity] export default class AuthModuleService< - TAuthIdentity extends AuthIdentity = AuthIdentity + TAuthIdentity extends AuthIdentity = AuthIdentity, + TProviderIdentity extends ProviderIdentity = ProviderIdentity > extends ModulesSdkUtils.abstractModuleServiceFactory< InjectedDependencies, AuthTypes.AuthIdentityDTO, { AuthIdentity: { dto: AuthTypes.AuthIdentityDTO } + ProviderIdentity: { dto: AuthTypes.ProviderIdentityDTO } } >(AuthIdentity, generateMethodForModels, entityNameToLinkableKeysMap) implements AuthTypes.IAuthModuleService { protected baseRepository_: DAL.RepositoryService protected authIdentityService_: ModulesSdkTypes.InternalModuleService + protected providerIdentityService_: ModulesSdkTypes.InternalModuleService protected readonly authProviderService_: AuthProviderService constructor( { authIdentityService, + providerIdentityService, authProviderService, baseRepository, }: InjectedDependencies, @@ -60,6 +66,7 @@ export default class AuthModuleService< this.baseRepository_ = baseRepository this.authIdentityService_ = authIdentityService this.authProviderService_ = authProviderService + this.providerIdentityService_ = providerIdentityService } __joinerConfig(): ModuleJoinerConfig { @@ -132,7 +139,7 @@ export default class AuthModuleService< return await this.authProviderService_.authenticate( provider, authenticationData, - this.getAuthIdentityProviderService() + this.getAuthIdentityProviderService(provider) ) } catch (error) { return { success: false, error: error.message } @@ -147,20 +154,29 @@ export default class AuthModuleService< return await this.authProviderService_.validateCallback( provider, authenticationData, - this.getAuthIdentityProviderService() + this.getAuthIdentityProviderService(provider) ) } catch (error) { return { success: false, error: error.message } } } - getAuthIdentityProviderService(): AuthIdentityProviderService { + getAuthIdentityProviderService( + provider: string + ): AuthIdentityProviderService { return { - retrieve: async ({ entity_id, provider }) => { - const authIdentities = await this.authIdentityService_.list({ - entity_id, - provider, - }) + retrieve: async ({ entity_id }) => { + const authIdentities = await this.authIdentityService_.list( + { + provider_identities: { + entity_id, + provider, + }, + }, + { + relations: ["provider_identities"], + } + ) if (!authIdentities.length) { throw new MedusaError( @@ -180,8 +196,26 @@ export default class AuthModuleService< authIdentities[0] ) }, - create: async (data: AuthTypes.CreateAuthIdentityDTO) => { - const createdAuthIdentity = await this.authIdentityService_.create(data) + + create: async (data: { + entity_id: string + provider_metadata?: Record + user_metadata?: Record + }) => { + const normalizedRequest = { + provider_identities: [ + { + entity_id: data.entity_id, + provider_metadata: data.provider_metadata, + user_metadata: data.user_metadata, + provider, + }, + ], + } + + const createdAuthIdentity = await this.authIdentityService_.create( + normalizedRequest + ) return await this.baseRepository_.serialize( createdAuthIdentity 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 131d8c43b3..8afc8fa228 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 @@ -22,7 +22,7 @@ describe("Email password auth provider", () => { it("return error if email is not passed", async () => { const resp = await emailpassService.authenticate( { body: { password: "otherpass" } }, - {} + {} as any ) expect(resp).toEqual({ @@ -34,7 +34,7 @@ describe("Email password auth provider", () => { it("return error if password is not passed", async () => { const resp = await emailpassService.authenticate( { body: { email: "test@admin.com" } }, - {} + {} as any ) expect(resp).toEqual({ @@ -50,18 +50,22 @@ describe("Email password auth provider", () => { const authServiceSpies = { retrieve: jest.fn().mockImplementation(() => { return { - entity_id: "test@admin.com", - provider: "emailpass", - provider_metadata: { - password: passwordHash.toString("base64"), - }, + provider_identities: [ + { + entity_id: "test@admin.com", + provider: "emailpass", + provider_metadata: { + password: passwordHash.toString("base64"), + }, + }, + ], } }), } const resp = await emailpassService.authenticate( { body: { email: "test@admin.com", password: "otherpass" } }, - authServiceSpies + authServiceSpies as any ) expect(authServiceSpies.retrieve).toHaveBeenCalled() @@ -78,18 +82,22 @@ describe("Email password auth provider", () => { const authServiceSpies = { retrieve: jest.fn().mockImplementation(() => { return { - entity_id: "test@admin.com", - provider: "emailpass", - provider_metadata: { - password: passwordHash.toString("base64"), - }, + provider_identities: [ + { + entity_id: "test@admin.com", + provider: "emailpass", + provider_metadata: { + password: passwordHash.toString("base64"), + }, + }, + ], } }), } const resp = await emailpassService.authenticate( { body: { email: "test@admin.com", password: "somepass" } }, - authServiceSpies + authServiceSpies as any ) expect(authServiceSpies.retrieve).toHaveBeenCalled() @@ -97,8 +105,12 @@ describe("Email password auth provider", () => { expect.objectContaining({ success: true, authIdentity: expect.objectContaining({ - entity_id: "test@admin.com", - provider_metadata: {}, + provider_identities: [ + expect.objectContaining({ + entity_id: "test@admin.com", + provider_metadata: {}, + }), + ], }), }) ) @@ -111,11 +123,15 @@ describe("Email password auth provider", () => { }), create: jest.fn().mockImplementation(() => { return { - entity_id: "test@admin.com", - provider: "emailpass", - provider_metadata: { - password: "somehash", - }, + provider_identities: [ + { + entity_id: "test@admin.com", + provider: "emailpass", + provider_metadata: { + password: "somehash", + }, + }, + ], } }), } @@ -128,7 +144,7 @@ describe("Email password auth provider", () => { expect(authServiceSpies.retrieve).toHaveBeenCalled() expect(authServiceSpies.create).toHaveBeenCalled() - expect(resp.authIdentity).toEqual( + expect(resp.authIdentity?.provider_identities?.[0]).toEqual( expect.objectContaining({ entity_id: "test@admin.com", provider_metadata: {}, diff --git a/packages/modules/providers/auth-emailpass/src/services/emailpass.ts b/packages/modules/providers/auth-emailpass/src/services/emailpass.ts index b79f0579c5..247c219da4 100644 --- a/packages/modules/providers/auth-emailpass/src/services/emailpass.ts +++ b/packages/modules/providers/auth-emailpass/src/services/emailpass.ts @@ -4,6 +4,7 @@ import { AuthenticationResponse, AuthenticationInput, AuthIdentityProviderService, + AuthIdentityDTO, } from "@medusajs/types" import { AbstractAuthModuleProvider, @@ -53,12 +54,11 @@ export class EmailPassAuthService extends AbstractAuthModuleProvider { error: "Email should be a string", } } - let authIdentity + let authIdentity: AuthIdentityDTO | undefined try { authIdentity = await authIdentityService.retrieve({ entity_id: email, - provider: this.provider, }) } catch (error) { if (error.type === MedusaError.Types.NOT_FOUND) { @@ -67,14 +67,16 @@ export class EmailPassAuthService extends AbstractAuthModuleProvider { const createdAuthIdentity = await authIdentityService.create({ entity_id: email, - provider: this.provider, provider_metadata: { password: passwordHash.toString("base64"), }, }) const copy = JSON.parse(JSON.stringify(createdAuthIdentity)) - delete copy.provider_metadata?.password + const providerIdentity = copy.provider_identities?.find( + (pi) => pi.provider === this.provider + )! + delete providerIdentity.provider_metadata?.password return { success: true, @@ -85,7 +87,10 @@ export class EmailPassAuthService extends AbstractAuthModuleProvider { return { success: false, error: error.message } } - const passwordHash = authIdentity.provider_metadata?.password + const providerIdentity = authIdentity.provider_identities?.find( + (pi) => pi.provider === this.provider + )! + const passwordHash = providerIdentity.provider_metadata?.password if (isString(passwordHash)) { const buf = Buffer.from(passwordHash as string, "base64") @@ -93,7 +98,10 @@ export class EmailPassAuthService extends AbstractAuthModuleProvider { if (success) { const copy = JSON.parse(JSON.stringify(authIdentity)) - delete copy.provider_metadata!.password + const providerIdentity = copy.provider_identities?.find( + (pi) => pi.provider === this.provider + )! + delete providerIdentity.provider_metadata?.password return { success, diff --git a/packages/modules/providers/auth-google/integration-tests/__tests__/services.spec.ts b/packages/modules/providers/auth-google/integration-tests/__tests__/services.spec.ts index da68a18677..60b54bc21a 100644 --- a/packages/modules/providers/auth-google/integration-tests/__tests__/services.spec.ts +++ b/packages/modules/providers/auth-google/integration-tests/__tests__/services.spec.ts @@ -158,8 +158,12 @@ describe("Google auth provider", () => { }), create: jest.fn().mockImplementation(() => { return { - entity_id: "test@admin.com", - provider: "google", + provider_identities: [ + { + entity_id: "test@admin.com", + provider: "google", + }, + ], } }), } @@ -177,8 +181,12 @@ describe("Google auth provider", () => { success: true, successRedirectUrl: baseUrl, authIdentity: { - entity_id: "test@admin.com", - provider: "google", + provider_identities: [ + { + entity_id: "test@admin.com", + provider: "google", + }, + ], }, }) }) @@ -187,8 +195,12 @@ describe("Google auth provider", () => { const authServiceSpies = { retrieve: jest.fn().mockImplementation(() => { return { - entity_id: "test@admin.com", - provider: "google", + provider_identities: [ + { + entity_id: "test@admin.com", + provider: "google", + }, + ], } }), create: jest.fn().mockImplementation(() => { @@ -209,8 +221,12 @@ describe("Google auth provider", () => { success: true, successRedirectUrl: baseUrl, authIdentity: { - entity_id: "test@admin.com", - provider: "google", + provider_identities: [ + { + entity_id: "test@admin.com", + provider: "google", + }, + ], }, }) }) diff --git a/packages/modules/providers/auth-google/src/services/google.ts b/packages/modules/providers/auth-google/src/services/google.ts index d83b2dcd0a..466a67a26c 100644 --- a/packages/modules/providers/auth-google/src/services/google.ts +++ b/packages/modules/providers/auth-google/src/services/google.ts @@ -130,13 +130,11 @@ export class GoogleAuthService extends AbstractAuthModuleProvider { try { authIdentity = await authIdentityService.retrieve({ entity_id, - provider: this.provider, }) } catch (error) { if (error.type === MedusaError.Types.NOT_FOUND) { const createdAuthIdentity = await authIdentityService.create({ entity_id, - provider: this.provider, user_metadata: userMetadata, }) authIdentity = createdAuthIdentity