diff --git a/.changeset/loud-dogs-learn.md b/.changeset/loud-dogs-learn.md new file mode 100644 index 0000000000..8bba72a539 --- /dev/null +++ b/.changeset/loud-dogs-learn.md @@ -0,0 +1,8 @@ +--- +"@medusajs/medusa": patch +"@medusajs/api-key": patch +"@medusajs/core-flows": patch +"@medusajs/link-modules": patch +--- + +feat: API key sales channel link diff --git a/integration-tests/api/__tests__/admin/api-key.spec.ts b/integration-tests/api/__tests__/admin/api-key.spec.ts new file mode 100644 index 0000000000..7822fd8045 --- /dev/null +++ b/integration-tests/api/__tests__/admin/api-key.spec.ts @@ -0,0 +1,273 @@ +import { ApiKeyType } from "@medusajs/utils" +import { medusaIntegrationTestRunner } from "medusa-test-utils" +import { createAdminUser } from "../../../helpers/create-admin-user" + +jest.setTimeout(50000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +medusaIntegrationTestRunner({ + env, + testSuite: ({ dbConnection, getContainer, api }) => { + describe("API Keys - Admin", () => { + let container + + beforeAll(async () => { + container = getContainer() + }) + + beforeEach(async () => { + await createAdminUser(dbConnection, adminHeaders, container) + }) + + it("should correctly implement the entire lifecycle of an api key", async () => { + const created = await api.post( + `/admin/api-keys`, + { + title: "Test Secret Key", + type: ApiKeyType.SECRET, + }, + adminHeaders + ) + + expect(created.status).toEqual(200) + expect(created.data.api_key).toEqual( + expect.objectContaining({ + id: created.data.api_key.id, + title: "Test Secret Key", + created_by: "admin_user", + }) + ) + // On create we get the token in raw form so we can store it. + expect(created.data.api_key.token).toContain("sk_") + + const updated = await api.post( + `/admin/api-keys/${created.data.api_key.id}`, + { + title: "Updated Secret Key", + }, + adminHeaders + ) + + expect(updated.status).toEqual(200) + expect(updated.data.api_key).toEqual( + expect.objectContaining({ + id: created.data.api_key.id, + title: "Updated Secret Key", + }) + ) + + const revoked = await api.post( + `/admin/api-keys/${created.data.api_key.id}/revoke`, + {}, + adminHeaders + ) + + expect(revoked.status).toEqual(200) + expect(revoked.data.api_key).toEqual( + expect.objectContaining({ + id: created.data.api_key.id, + revoked_by: "admin_user", + }) + ) + expect(revoked.data.api_key.revoked_at).toBeTruthy() + + const deleted = await api.delete( + `/admin/api-keys/${created.data.api_key.id}`, + adminHeaders + ) + const listedApiKeys = await api.get(`/admin/api-keys`, adminHeaders) + + expect(deleted.status).toEqual(200) + expect(listedApiKeys.data.api_keys).toHaveLength(0) + }) + + it("can use a secret api key for authentication", async () => { + const created = await api.post( + `/admin/api-keys`, + { + title: "Test Secret Key", + type: ApiKeyType.SECRET, + }, + adminHeaders + ) + + const createdRegion = await api.post( + `/admin/regions`, + { + name: "Test Region", + currency_code: "usd", + countries: ["us", "ca"], + }, + { + auth: { + username: created.data.api_key.token, + }, + } + ) + + expect(createdRegion.status).toEqual(200) + expect(createdRegion.data.region.name).toEqual("Test Region") + }) + + it("falls back to other mode of authentication when an api key is not valid", async () => { + const created = await api.post( + `/admin/api-keys`, + { + title: "Test Secret Key", + type: ApiKeyType.SECRET, + }, + adminHeaders + ) + + await api.post( + `/admin/api-keys/${created.data.api_key.id}/revoke`, + {}, + adminHeaders + ) + + const err = await api + .post( + `/admin/regions`, + { + name: "Test Region", + currency_code: "usd", + countries: ["us", "ca"], + }, + { + auth: { + username: created.data.api_key.token, + }, + } + ) + .catch((e) => e.message) + + const createdRegion = await api.post( + `/admin/regions`, + { + name: "Test Region", + currency_code: "usd", + countries: ["us", "ca"], + }, + { + auth: { + username: created.data.api_key.token, + }, + ...adminHeaders, + } + ) + + expect(err).toEqual("Request failed with status code 401") + expect(createdRegion.status).toEqual(200) + expect(createdRegion.data.region.name).toEqual("Test Region") + }) + + it("should associate sales channels with a publishable API key", async () => { + const salesChannelRes = await api.post( + `/admin/sales-channels`, + { + name: "Test Sales Channel", + }, + adminHeaders + ) + + const { sales_channel } = salesChannelRes.data + + const apiKeyRes = await api.post( + `/admin/api-keys`, + { + title: "Test publishable KEY", + type: ApiKeyType.PUBLISHABLE, + }, + adminHeaders + ) + + const { api_key } = apiKeyRes.data + + const keyWithChannelsRes = await api.post( + `/admin/api-keys/${api_key.id}/sales-channels/batch/add`, + { + sales_channel_ids: [sales_channel.id], + }, + adminHeaders + ) + + const { api_key: keyWithChannels } = keyWithChannelsRes.data + + expect(keyWithChannelsRes.status).toEqual(200) + expect(keyWithChannels.title).toEqual("Test publishable KEY") + expect(keyWithChannels.sales_channels).toEqual([ + expect.objectContaining({ + id: sales_channel.id, + name: "Test Sales Channel", + }), + ]) + }) + + it("should throw if API key is not a publishable key", async () => { + const salesChannelRes = await api.post( + `/admin/sales-channels`, + { + name: "Test Sales Channel", + }, + adminHeaders + ) + + const { sales_channel } = salesChannelRes.data + + const apiKeyRes = await api.post( + `/admin/api-keys`, + { + title: "Test secret KEY", + type: ApiKeyType.SECRET, + }, + adminHeaders + ) + + const errorRes = await api + .post( + `/admin/api-keys/${apiKeyRes.data.api_key.id}/sales-channels/batch/add`, + { + sales_channel_ids: [sales_channel.id], + }, + adminHeaders + ) + .catch((err) => err) + + expect(errorRes.response.status).toEqual(400) + expect(errorRes.response.data.message).toEqual( + "Sales channels can only be associated with publishable API keys" + ) + }) + + it("should throw if sales channel does not exist", async () => { + const apiKeyRes = await api.post( + `/admin/api-keys`, + { + title: "Test publishable KEY", + type: ApiKeyType.PUBLISHABLE, + }, + adminHeaders + ) + + const errorRes = await api + .post( + `/admin/api-keys/${apiKeyRes.data.api_key.id}/sales-channels/batch/add`, + { + sales_channel_ids: ["phony"], + }, + adminHeaders + ) + .catch((err) => err) + + expect(errorRes.response.status).toEqual(400) + expect(errorRes.response.data.message).toEqual( + "Sales channels with IDs phony do not exist" + ) + }) + }) + }, +}) diff --git a/integration-tests/modules/__tests__/api-key/admin/api-key.spec.ts b/integration-tests/modules/__tests__/api-key/admin/api-key.spec.ts deleted file mode 100644 index 3196c79851..0000000000 --- a/integration-tests/modules/__tests__/api-key/admin/api-key.spec.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { ApiKeyType } from "@medusajs/utils" -import { IRegionModuleService } from "@medusajs/types" -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { createAdminUser } from "../../../../helpers/create-admin-user" -import { medusaIntegrationTestRunner } from "medusa-test-utils" - -jest.setTimeout(50000) - -const env = { MEDUSA_FF_MEDUSA_V2: true } -const adminHeaders = { - headers: { "x-medusa-access-token": "test_token" }, -} - -medusaIntegrationTestRunner({ - env, - testSuite: ({ dbConnection, getContainer, api }) => { - describe("API Keys - Admin", () => { - let regionService: IRegionModuleService - let container - - beforeAll(async () => { - container = getContainer() - regionService = container.resolve( - ModuleRegistrationName.REGION - ) as IRegionModuleService - }) - - beforeEach(async () => { - await createAdminUser(dbConnection, adminHeaders, container) - }) - - afterEach(async () => { - // TODO: Once teardown doesn't skip constraint checks and cascades, we can remove this - const existingRegions = await regionService.list({}) - await regionService.delete(existingRegions.map((r) => r.id)) - }) - - it("should correctly implement the entire lifecycle of an api key", async () => { - const created = await api.post( - `/admin/api-keys`, - { - title: "Test Secret Key", - type: ApiKeyType.SECRET, - }, - adminHeaders - ) - - expect(created.status).toEqual(200) - expect(created.data.apiKey).toEqual( - expect.objectContaining({ - id: created.data.apiKey.id, - title: "Test Secret Key", - created_by: "admin_user", - }) - ) - // On create we get the token in raw form so we can store it. - expect(created.data.apiKey.token).toContain("sk_") - - const updated = await api.post( - `/admin/api-keys/${created.data.apiKey.id}`, - { - title: "Updated Secret Key", - }, - adminHeaders - ) - - expect(updated.status).toEqual(200) - expect(updated.data.apiKey).toEqual( - expect.objectContaining({ - id: created.data.apiKey.id, - title: "Updated Secret Key", - }) - ) - - const revoked = await api.post( - `/admin/api-keys/${created.data.apiKey.id}/revoke`, - {}, - adminHeaders - ) - - expect(revoked.status).toEqual(200) - expect(revoked.data.apiKey).toEqual( - expect.objectContaining({ - id: created.data.apiKey.id, - revoked_by: "admin_user", - }) - ) - expect(revoked.data.apiKey.revoked_at).toBeTruthy() - - const deleted = await api.delete( - `/admin/api-keys/${created.data.apiKey.id}`, - adminHeaders - ) - const listedApiKeys = await api.get(`/admin/api-keys`, adminHeaders) - - expect(deleted.status).toEqual(200) - expect(listedApiKeys.data.apiKeys).toHaveLength(0) - }) - - it("can use a secret api key for authentication", async () => { - const created = await api.post( - `/admin/api-keys`, - { - title: "Test Secret Key", - type: ApiKeyType.SECRET, - }, - adminHeaders - ) - - const createdRegion = await api.post( - `/admin/regions`, - { - name: "Test Region", - currency_code: "usd", - countries: ["us", "ca"], - }, - { - auth: { - username: created.data.apiKey.token, - }, - } - ) - - expect(createdRegion.status).toEqual(200) - expect(createdRegion.data.region.name).toEqual("Test Region") - }) - - it("falls back to other mode of authentication when an api key is not valid", async () => { - const created = await api.post( - `/admin/api-keys`, - { - title: "Test Secret Key", - type: ApiKeyType.SECRET, - }, - adminHeaders - ) - - await api.post( - `/admin/api-keys/${created.data.apiKey.id}/revoke`, - {}, - adminHeaders - ) - - const err = await api - .post( - `/admin/regions`, - { - name: "Test Region", - currency_code: "usd", - countries: ["us", "ca"], - }, - { - auth: { - username: created.data.apiKey.token, - }, - } - ) - .catch((e) => e.message) - - const createdRegion = await api.post( - `/admin/regions`, - { - name: "Test Region", - currency_code: "usd", - countries: ["us", "ca"], - }, - { - auth: { - username: created.data.apiKey.token, - }, - ...adminHeaders, - } - ) - - expect(err).toEqual("Request failed with status code 401") - expect(createdRegion.status).toEqual(200) - expect(createdRegion.data.region.name).toEqual("Test Region") - }) - }) - }, -}) diff --git a/packages/api-key/src/joiner-config.ts b/packages/api-key/src/joiner-config.ts index ca9c9f2fd6..e80a16c2f0 100644 --- a/packages/api-key/src/joiner-config.ts +++ b/packages/api-key/src/joiner-config.ts @@ -3,7 +3,9 @@ import { ModuleJoinerConfig } from "@medusajs/types" import { MapToConfig } from "@medusajs/utils" import ApiKey from "./models/api-key" -export const LinkableKeys: Record = {} +export const LinkableKeys: Record = { + api_key_id: ApiKey.name, +} const entityLinkableKeysMap: MapToConfig = {} Object.entries(LinkableKeys).forEach(([key, value]) => { diff --git a/packages/api-key/src/models/api-key.ts b/packages/api-key/src/models/api-key.ts index 28099fcaf9..d8ab241378 100644 --- a/packages/api-key/src/models/api-key.ts +++ b/packages/api-key/src/models/api-key.ts @@ -6,10 +6,10 @@ import { import { BeforeCreate, Entity, + Enum, OnInit, PrimaryKey, Property, - Enum, } from "@mikro-orm/core" const TypeIndex = createPsqlIndexStatementHelper({ diff --git a/packages/core-flows/src/api-key/steps/associate-sales-channels-with-publishable-keys.ts b/packages/core-flows/src/api-key/steps/associate-sales-channels-with-publishable-keys.ts new file mode 100644 index 0000000000..255444aeba --- /dev/null +++ b/packages/core-flows/src/api-key/steps/associate-sales-channels-with-publishable-keys.ts @@ -0,0 +1,47 @@ +import { Modules } from "@medusajs/modules-sdk" +import { ContainerRegistrationKeys } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +interface StepInput { + links: { + api_key_id: string + sales_channel_ids: string[] + }[] +} + +export const associateApiKeysWithSalesChannelsStepId = + "associate-sales-channels-with-api-keys" +export const associateApiKeysWithSalesChannelsStep = createStep( + associateApiKeysWithSalesChannelsStepId, + async (input: StepInput, { container }) => { + const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) + + const links = input.links + .map((link) => { + return link.sales_channel_ids.map((id) => { + return { + [Modules.API_KEY]: { + publishable_key_id: link.api_key_id, + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: id, + }, + } + }) + }) + .flat() + + const createdLinks = await remoteLink.create(links) + + return new StepResponse(createdLinks, links) + }, + async (links, { container }) => { + if (!links) { + return + } + + const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) + + await remoteLink.dismiss(links) + } +) diff --git a/packages/core-flows/src/api-key/steps/index.ts b/packages/core-flows/src/api-key/steps/index.ts index 829543dbbb..c5c4100a90 100644 --- a/packages/core-flows/src/api-key/steps/index.ts +++ b/packages/core-flows/src/api-key/steps/index.ts @@ -1,4 +1,6 @@ +export * from "./associate-sales-channels-with-publishable-keys" export * from "./create-api-keys" export * from "./delete-api-keys" -export * from "./update-api-keys" export * from "./revoke-api-keys" +export * from "./update-api-keys" +export * from "./validate-sales-channel-exists" diff --git a/packages/core-flows/src/api-key/steps/validate-sales-channel-exists.ts b/packages/core-flows/src/api-key/steps/validate-sales-channel-exists.ts new file mode 100644 index 0000000000..2f9388cb06 --- /dev/null +++ b/packages/core-flows/src/api-key/steps/validate-sales-channel-exists.ts @@ -0,0 +1,37 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ISalesChannelModuleService } from "@medusajs/types" +import { MedusaError, arrayDifference } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +interface StepInput { + sales_channel_ids: string[] +} + +export const validateSalesChannelsExistStepId = "validate-sales-channels-exist" +export const validateSalesChannelsExistStep = createStep( + validateSalesChannelsExistStepId, + async (data: StepInput, { container }) => { + const salesChannelModuleService = + container.resolve( + ModuleRegistrationName.SALES_CHANNEL + ) + + const salesChannels = await salesChannelModuleService.list( + { id: data.sales_channel_ids }, + { select: ["id"] } + ) + + const salesChannelIds = salesChannels.map((v) => v.id) + + const notFound = arrayDifference(data.sales_channel_ids, salesChannelIds) + + if (notFound.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Sales channels with IDs ${notFound.join(", ")} do not exist` + ) + } + + return new StepResponse(salesChannelIds) + } +) diff --git a/packages/core-flows/src/api-key/workflows/add-sales-channels-to-publishable-key.ts b/packages/core-flows/src/api-key/workflows/add-sales-channels-to-publishable-key.ts new file mode 100644 index 0000000000..2e43647175 --- /dev/null +++ b/packages/core-flows/src/api-key/workflows/add-sales-channels-to-publishable-key.ts @@ -0,0 +1,31 @@ +import { + WorkflowData, + createWorkflow, + transform, +} from "@medusajs/workflows-sdk" +import { + associateApiKeysWithSalesChannelsStep, + validateSalesChannelsExistStep, +} from "../steps" + +type WorkflowInput = { + data: { + api_key_id: string + sales_channel_ids: string[] + }[] +} + +export const addSalesChannelsToApiKeyWorkflowId = + "add-sales-channels-to-api-key" +export const addSalesChannelsToApiKeyWorkflow = createWorkflow( + addSalesChannelsToApiKeyWorkflowId, + (input: WorkflowData) => { + const salesChannelIds = transform(input.data, (data) => + data.map((d) => d.sales_channel_ids).flat() + ) + validateSalesChannelsExistStep({ + sales_channel_ids: salesChannelIds, + }) + associateApiKeysWithSalesChannelsStep({ links: input.data }) + } +) diff --git a/packages/core-flows/src/api-key/workflows/index.ts b/packages/core-flows/src/api-key/workflows/index.ts index 829543dbbb..55b998523b 100644 --- a/packages/core-flows/src/api-key/workflows/index.ts +++ b/packages/core-flows/src/api-key/workflows/index.ts @@ -2,3 +2,4 @@ export * from "./create-api-keys" export * from "./delete-api-keys" export * from "./update-api-keys" export * from "./revoke-api-keys" +export * from "./add-sales-channels-to-publishable-key" diff --git a/packages/link-modules/src/definitions/publishable-api-key-sales-channel.ts b/packages/link-modules/src/definitions/publishable-api-key-sales-channel.ts index 86a50db58a..d6f9bc8dc1 100644 --- a/packages/link-modules/src/definitions/publishable-api-key-sales-channel.ts +++ b/packages/link-modules/src/definitions/publishable-api-key-sales-channel.ts @@ -1,6 +1,6 @@ +import { Modules } from "@medusajs/modules-sdk" import { ModuleJoinerConfig } from "@medusajs/types" import { LINKS } from "../links" -import { Modules } from "@medusajs/modules-sdk" export const PublishableApiKeySalesChannel: ModuleJoinerConfig = { serviceName: LINKS.PublishableApiKeySalesChannel, @@ -21,14 +21,12 @@ export const PublishableApiKeySalesChannel: ModuleJoinerConfig = { relationships: [ { serviceName: Modules.API_KEY, - isInternalService: true, primaryKey: "id", foreignKey: "publishable_key_id", alias: "api_key", }, { serviceName: Modules.SALES_CHANNEL, - isInternalService: true, primaryKey: "id", foreignKey: "sales_channel_id", alias: "sales_channel", @@ -42,7 +40,6 @@ export const PublishableApiKeySalesChannel: ModuleJoinerConfig = { }, relationship: { serviceName: LINKS.PublishableApiKeySalesChannel, - isInternalService: true, primaryKey: "publishable_key_id", foreignKey: "id", alias: "sales_channels_link", @@ -56,7 +53,6 @@ export const PublishableApiKeySalesChannel: ModuleJoinerConfig = { }, relationship: { serviceName: LINKS.PublishableApiKeySalesChannel, - isInternalService: true, primaryKey: "sales_channel_id", foreignKey: "id", alias: "api_keys_link", diff --git a/packages/medusa/src/api-v2/admin/api-keys/[id]/revoke/route.ts b/packages/medusa/src/api-v2/admin/api-keys/[id]/revoke/route.ts index d572e8d826..61f81ebbc4 100644 --- a/packages/medusa/src/api-v2/admin/api-keys/[id]/revoke/route.ts +++ b/packages/medusa/src/api-v2/admin/api-keys/[id]/revoke/route.ts @@ -1,18 +1,19 @@ +import { revokeApiKeysWorkflow } from "@medusajs/core-flows" +import { RevokeApiKeyDTO } from "@medusajs/types" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" import { AuthenticatedMedusaRequest, MedusaResponse, } from "../../../../../types/routing" -import { RevokeApiKeyDTO } from "@medusajs/types" -import { revokeApiKeysWorkflow } from "@medusajs/core-flows" - export const POST = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const id = req.params.id - - const { result, errors } = await revokeApiKeysWorkflow(req.scope).run({ + const { errors } = await revokeApiKeysWorkflow(req.scope).run({ input: { selector: { id: req.params.id }, revoke: { @@ -27,5 +28,17 @@ export const POST = async ( throw errors[0].error } - res.status(200).json({ apiKey: result[0] }) + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "api_key", + variables: { + id: req.params.id, + }, + fields: req.remoteQueryConfig.fields, + }) + + const [apiKey] = await remoteQuery(queryObject) + + res.status(200).json({ api_key: apiKey }) } diff --git a/packages/medusa/src/api-v2/admin/api-keys/[id]/route.ts b/packages/medusa/src/api-v2/admin/api-keys/[id]/route.ts index 5e0cb25c9d..1982d7dba7 100644 --- a/packages/medusa/src/api-v2/admin/api-keys/[id]/route.ts +++ b/packages/medusa/src/api-v2/admin/api-keys/[id]/route.ts @@ -1,21 +1,24 @@ -import { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "../../../../types/routing" import { deleteApiKeysWorkflow, updateApiKeysWorkflow, } from "@medusajs/core-flows" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../types/routing" import { UpdateApiKeyDTO } from "@medusajs/types" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" import { defaultAdminApiKeyFields } from "../query-config" -import { remoteQueryObjectFromString } from "@medusajs/utils" export const GET = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const remoteQuery = req.scope.resolve("remoteQuery") + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) const variables = { id: req.params.id } @@ -46,7 +49,19 @@ export const POST = async ( throw errors[0].error } - res.status(200).json({ apiKey: result[0] }) + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "api_key", + variables: { + id: req.params.id, + }, + fields: req.remoteQueryConfig.fields, + }) + + const [apiKey] = await remoteQuery(queryObject) + + res.status(200).json({ api_key: apiKey }) } export const DELETE = async ( diff --git a/packages/medusa/src/api-v2/admin/api-keys/[id]/sales-channels/batch/add/route.ts b/packages/medusa/src/api-v2/admin/api-keys/[id]/sales-channels/batch/add/route.ts new file mode 100644 index 0000000000..e729c267cb --- /dev/null +++ b/packages/medusa/src/api-v2/admin/api-keys/[id]/sales-channels/batch/add/route.ts @@ -0,0 +1,62 @@ +import { addSalesChannelsToApiKeyWorkflow } from "@medusajs/core-flows" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + ContainerRegistrationKeys, + MedusaError, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../../../../types/routing" +import { AdminPostApiKeysApiKeySalesChannelsBatchReq } from "../../../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const body = req.validatedBody as AdminPostApiKeysApiKeySalesChannelsBatchReq + + const apiKeyModule = req.scope.resolve(ModuleRegistrationName.API_KEY) + + const apiKey = await apiKeyModule.retrieve(req.params.id) + + if (apiKey.type !== "publishable") { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Sales channels can only be associated with publishable API keys" + ) + } + + const workflowInput = { + data: [ + { + api_key_id: req.params.id, + sales_channel_ids: body.sales_channel_ids, + }, + ], + } + + const { errors } = await addSalesChannelsToApiKeyWorkflow(req.scope).run({ + input: workflowInput, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const query = remoteQueryObjectFromString({ + entryPoint: "api_key", + fields: req.remoteQueryConfig.fields, + variables: { + id: req.params.id, + }, + }) + + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const [result] = await remoteQuery(query) + + res.status(200).json({ api_key: result }) +} diff --git a/packages/medusa/src/api-v2/admin/api-keys/middlewares.ts b/packages/medusa/src/api-v2/admin/api-keys/middlewares.ts index 819687720d..c1c12e7cdb 100644 --- a/packages/medusa/src/api-v2/admin/api-keys/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/api-keys/middlewares.ts @@ -1,13 +1,14 @@ import * as QueryConfig from "./query-config" +import { transformBody, transformQuery } from "../../../api/middlewares" import { AdminGetApiKeysApiKeyParams, AdminGetApiKeysParams, AdminPostApiKeysApiKeyReq, + AdminPostApiKeysApiKeySalesChannelsBatchReq, AdminPostApiKeysReq, AdminRevokeApiKeysApiKeyReq, } from "./validators" -import { transformBody, transformQuery } from "../../../api/middlewares" import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { authenticate } from "../../../utils/authenticate-middleware" @@ -40,12 +41,24 @@ export const adminApiKeyRoutesMiddlewares: MiddlewareRoute[] = [ { method: ["POST"], matcher: "/admin/api-keys", - middlewares: [transformBody(AdminPostApiKeysReq)], + middlewares: [ + transformQuery( + AdminGetApiKeysApiKeyParams, + QueryConfig.retrieveTransformQueryConfig + ), + transformBody(AdminPostApiKeysReq), + ], }, { method: ["POST"], matcher: "/admin/api-keys/:id", - middlewares: [transformBody(AdminPostApiKeysApiKeyReq)], + middlewares: [ + transformQuery( + AdminGetApiKeysApiKeyParams, + QueryConfig.retrieveTransformQueryConfig + ), + transformBody(AdminPostApiKeysApiKeyReq), + ], }, { method: ["DELETE"], @@ -55,6 +68,23 @@ export const adminApiKeyRoutesMiddlewares: MiddlewareRoute[] = [ { method: ["POST"], matcher: "/admin/api-keys/:id/revoke", - middlewares: [transformBody(AdminRevokeApiKeysApiKeyReq)], + middlewares: [ + transformQuery( + AdminGetApiKeysApiKeyParams, + QueryConfig.retrieveTransformQueryConfig + ), + transformBody(AdminRevokeApiKeysApiKeyReq), + ], + }, + { + method: ["POST"], + matcher: "/admin/api-keys/:id/sales-channels/batch/add", + middlewares: [ + transformQuery( + AdminGetApiKeysApiKeyParams, + QueryConfig.retrieveTransformQueryConfig + ), + transformBody(AdminPostApiKeysApiKeySalesChannelsBatchReq), + ], }, ] diff --git a/packages/medusa/src/api-v2/admin/api-keys/query-config.ts b/packages/medusa/src/api-v2/admin/api-keys/query-config.ts index 9160898416..41a60ebcad 100644 --- a/packages/medusa/src/api-v2/admin/api-keys/query-config.ts +++ b/packages/medusa/src/api-v2/admin/api-keys/query-config.ts @@ -1,5 +1,3 @@ -export const defaultAdminApiKeyRelations = [] -export const allowedAdminApiKeyRelations = [] export const defaultAdminApiKeyFields = [ "id", "title", @@ -11,16 +9,17 @@ export const defaultAdminApiKeyFields = [ "created_by", "revoked_at", "revoked_by", + "sales_channels.id", + "sales_channels.name", ] export const retrieveTransformQueryConfig = { - defaultFields: defaultAdminApiKeyFields, - defaultRelations: defaultAdminApiKeyRelations, - allowedRelations: allowedAdminApiKeyRelations, + defaults: defaultAdminApiKeyFields, isList: false, } export const listTransformQueryConfig = { + ...retrieveTransformQueryConfig, defaultLimit: 20, isList: true, } diff --git a/packages/medusa/src/api-v2/admin/api-keys/route.ts b/packages/medusa/src/api-v2/admin/api-keys/route.ts index 3e20b89033..217c5dddfb 100644 --- a/packages/medusa/src/api-v2/admin/api-keys/route.ts +++ b/packages/medusa/src/api-v2/admin/api-keys/route.ts @@ -1,34 +1,33 @@ +import { createApiKeysWorkflow } from "@medusajs/core-flows" +import { CreateApiKeyDTO } from "@medusajs/types" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" import { AuthenticatedMedusaRequest, MedusaResponse, } from "../../../types/routing" -import { CreateApiKeyDTO } from "@medusajs/types" -import { createApiKeysWorkflow } from "@medusajs/core-flows" -import { defaultAdminApiKeyFields } from "./query-config" -import { remoteQueryObjectFromString } from "@medusajs/utils" - export const GET = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const remoteQuery = req.scope.resolve("remoteQuery") + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) const queryObject = remoteQueryObjectFromString({ entryPoint: "api_key", variables: { filters: req.filterableFields, - order: req.listConfig.order, - skip: req.listConfig.skip, - take: req.listConfig.take, + ...req.remoteQueryConfig.pagination, }, - fields: defaultAdminApiKeyFields, + fields: req.remoteQueryConfig.fields, }) const { rows: apiKeys, metadata } = await remoteQuery(queryObject) res.json({ - apiKeys, + api_keys: apiKeys, count: metadata.count, offset: metadata.skip, limit: metadata.take, @@ -55,5 +54,7 @@ export const POST = async ( throw errors[0].error } - res.status(200).json({ apiKey: result[0] }) + // We cannot use remoteQuery here, as we need to show the secret key in the response (and never again) + // And the only time we get to see the secret, is when we create it + res.status(200).json({ api_key: result[0] }) } diff --git a/packages/medusa/src/api-v2/admin/api-keys/validators.ts b/packages/medusa/src/api-v2/admin/api-keys/validators.ts index fd8d1a0d22..8cf3f9b3f6 100644 --- a/packages/medusa/src/api-v2/admin/api-keys/validators.ts +++ b/packages/medusa/src/api-v2/admin/api-keys/validators.ts @@ -1,4 +1,4 @@ -import { OperatorMap } from "@medusajs/types" +import { ApiKeyType } from "@medusajs/utils" import { Type } from "class-transformer" import { IsArray, @@ -9,8 +9,6 @@ import { ValidateNested, } from "class-validator" import { FindParams, extendedFindParamsMixin } from "../../../types/common" -import { OperatorMapValidator } from "../../../types/validators/operator-map" -import { ApiKeyType } from "@medusajs/utils" export class AdminGetApiKeysApiKeyParams extends FindParams {} /** @@ -80,3 +78,8 @@ export class AdminRevokeApiKeysApiKeyReq { } export class AdminDeleteApiKeysApiKeyReq {} + +export class AdminPostApiKeysApiKeySalesChannelsBatchReq { + @IsArray() + sales_channel_ids: string[] +} diff --git a/packages/medusa/src/api-v2/admin/stock-locations/[id]/route.ts b/packages/medusa/src/api-v2/admin/stock-locations/[id]/route.ts index d7326c6cac..7dec2e6b54 100644 --- a/packages/medusa/src/api-v2/admin/stock-locations/[id]/route.ts +++ b/packages/medusa/src/api-v2/admin/stock-locations/[id]/route.ts @@ -4,10 +4,9 @@ import { } from "@medusajs/utils" import { MedusaRequest, MedusaResponse } from "../../../../types/routing" -import { AdminPostStockLocationsLocationReq } from "../validators" +import { deleteStockLocationsWorkflow, updateStockLocationsWorkflow } from "@medusajs/core-flows" import { MedusaError } from "@medusajs/utils" -import { deleteStockLocationsWorkflow } from "@medusajs/core-flows" -import { updateStockLocationsWorkflow } from "@medusajs/core-flows" +import { AdminPostStockLocationsLocationReq } from "../validators" export const POST = async ( req: MedusaRequest, diff --git a/packages/medusa/src/joiner-configs/index.ts b/packages/medusa/src/joiner-configs/index.ts index 74555665ea..e2b90d82a8 100644 --- a/packages/medusa/src/joiner-configs/index.ts +++ b/packages/medusa/src/joiner-configs/index.ts @@ -1,3 +1 @@ -export * as publishableApiKey from "./publishable-api-key-service" export * as shippingProfile from "./shipping-profile-service" - diff --git a/packages/medusa/src/joiner-configs/publishable-api-key-service.ts b/packages/medusa/src/joiner-configs/publishable-api-key-service.ts deleted file mode 100644 index e0ada9f421..0000000000 --- a/packages/medusa/src/joiner-configs/publishable-api-key-service.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ModuleJoinerConfig } from "@medusajs/types" - -export default { - serviceName: "publishableApiKeyService", - primaryKeys: ["id"], - linkableKeys: { publishable_key_id: "PublishableApiKey" }, - schema: ` - scalar Date - scalar JSON - - type PublishableApiKey { - id: ID! - sales_channel_id: String! - publishable_key_id: String! - created_at: Date! - updated_at: Date! - deleted_at: Date - } - `, - alias: [ - { - name: ["publishable_api_key", "publishable_api_keys"], - args: { - entity: "PublishableApiKey", - }, - }, - ], -} as ModuleJoinerConfig