feat: API key sales channel link (#6851)
What - Add link between API key and sales channels - Add API route for batch adding sales channels to a publishable API key - Clean up API key API routes responses - Move API key test suite from `integration-tests/modules` to `integration-tests/api`
This commit is contained in:
8
.changeset/loud-dogs-learn.md
Normal file
8
.changeset/loud-dogs-learn.md
Normal file
@@ -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
|
||||
273
integration-tests/api/__tests__/admin/api-key.spec.ts
Normal file
273
integration-tests/api/__tests__/admin/api-key.spec.ts
Normal file
@@ -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"
|
||||
)
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -3,7 +3,9 @@ import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { MapToConfig } from "@medusajs/utils"
|
||||
import ApiKey from "./models/api-key"
|
||||
|
||||
export const LinkableKeys: Record<string, string> = {}
|
||||
export const LinkableKeys: Record<string, string> = {
|
||||
api_key_id: ApiKey.name,
|
||||
}
|
||||
|
||||
const entityLinkableKeysMap: MapToConfig = {}
|
||||
Object.entries(LinkableKeys).forEach(([key, value]) => {
|
||||
|
||||
@@ -6,10 +6,10 @@ import {
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
Enum,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
Enum,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
const TypeIndex = createPsqlIndexStatementHelper({
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
@@ -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"
|
||||
|
||||
@@ -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<ISalesChannelModuleService>(
|
||||
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)
|
||||
}
|
||||
)
|
||||
@@ -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<WorkflowInput>) => {
|
||||
const salesChannelIds = transform(input.data, (data) =>
|
||||
data.map((d) => d.sales_channel_ids).flat()
|
||||
)
|
||||
validateSalesChannelsExistStep({
|
||||
sales_channel_ids: salesChannelIds,
|
||||
})
|
||||
associateApiKeysWithSalesChannelsStep({ links: input.data })
|
||||
}
|
||||
)
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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] })
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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<AdminPostStockLocationsLocationReq>,
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
export * as publishableApiKey from "./publishable-api-key-service"
|
||||
export * as shippingProfile from "./shipping-profile-service"
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user