diff --git a/integration-tests/http/__tests__/api-key/admin/api-key.spec.ts b/integration-tests/http/__tests__/api-key/admin/api-key.spec.ts
index b193127340..841d49d58f 100644
--- a/integration-tests/http/__tests__/api-key/admin/api-key.spec.ts
+++ b/integration-tests/http/__tests__/api-key/admin/api-key.spec.ts
@@ -1,4 +1,4 @@
-import { ApiKeyType, ContainerRegistrationKeys } from "@medusajs/utils"
+import { ApiKeyType } from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import {
adminHeaders,
@@ -372,9 +372,7 @@ medusaIntegrationTestRunner({
it("should detach sales channels from a publishable API key on delete", async () => {
const salesChannelRes = await api.post(
`/admin/sales-channels`,
- {
- name: "Test Sales Channel",
- },
+ { name: "Test Sales Channel" },
adminHeaders
)
@@ -393,9 +391,7 @@ medusaIntegrationTestRunner({
const keyWithChannelsRes = await api.post(
`/admin/api-keys/${api_key.id}/sales-channels`,
- {
- add: [sales_channel.id],
- },
+ { add: [sales_channel.id] },
adminHeaders
)
@@ -415,6 +411,21 @@ medusaIntegrationTestRunner({
}),
])
+ const revoked = await api.post(
+ `/admin/api-keys/${api_key.id}/revoke`,
+ {},
+ adminHeaders
+ )
+
+ expect(revoked.status).toEqual(200)
+ expect(revoked.data.api_key).toEqual(
+ expect.objectContaining({
+ id: api_key.id,
+ revoked_by: expect.stringMatching(/^user_*/),
+ })
+ )
+ expect(revoked.data.api_key.revoked_at).toBeTruthy()
+
await api.delete(`/admin/api-keys/${api_key.id}`, adminHeaders)
const deletedApiKeys = await api.get(
diff --git a/integration-tests/http/__tests__/api-key/admin/publishable-key.spec.ts b/integration-tests/http/__tests__/api-key/admin/publishable-key.spec.ts
index aec367cb9b..ca1a08aff5 100644
--- a/integration-tests/http/__tests__/api-key/admin/publishable-key.spec.ts
+++ b/integration-tests/http/__tests__/api-key/admin/publishable-key.spec.ts
@@ -129,6 +129,12 @@ medusaIntegrationTestRunner({
describe("DELETE /admin/api-keys/:id", () => {
it("delete a publishable key", async () => {
+ await api.post(
+ `/admin/api-keys/${pubKey1.id}/revoke`,
+ {},
+ adminHeaders
+ )
+
const response1 = await api.delete(
`/admin/api-keys/${pubKey1.id}`,
adminHeaders
diff --git a/packages/admin/dashboard/src/routes/api-key-management/api-key-management-detail/components/api-key-general-section/api-key-general-section.tsx b/packages/admin/dashboard/src/routes/api-key-management/api-key-management-detail/components/api-key-general-section/api-key-general-section.tsx
index 287f99782d..89e8fa9db1 100644
--- a/packages/admin/dashboard/src/routes/api-key-management/api-key-management-detail/components/api-key-general-section/api-key-general-section.tsx
+++ b/packages/admin/dashboard/src/routes/api-key-management/api-key-management-detail/components/api-key-general-section/api-key-general-section.tsx
@@ -12,7 +12,10 @@ import {
} from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
-import { ActionMenu } from "../../../../../components/common/action-menu"
+import {
+ Action,
+ ActionMenu,
+} from "../../../../../components/common/action-menu"
import { Skeleton } from "../../../../../components/common/skeleton"
import { UserLink } from "../../../../../components/common/user-link"
import {
@@ -97,11 +100,12 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
})
}
- const dangerousActions = [
+ const dangerousActions: Action[] = [
{
icon: ,
label: t("actions.delete"),
onClick: handleDelete,
+ disabled: !apiKey.revoked_at,
},
]
@@ -110,6 +114,7 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
icon: ,
label: t("apiKeyManagement.actions.revoke"),
onClick: handleRevoke,
+ disabled: !!apiKey.revoked_at,
})
}
diff --git a/packages/admin/dashboard/src/routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-row-actions.tsx b/packages/admin/dashboard/src/routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-row-actions.tsx
index 5e59cbfed0..1ef1e38ac9 100644
--- a/packages/admin/dashboard/src/routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-row-actions.tsx
+++ b/packages/admin/dashboard/src/routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-row-actions.tsx
@@ -107,11 +107,13 @@ export const ApiKeyRowActions = ({
icon: ,
label: t("apiKeyManagement.actions.revoke"),
onClick: handleRevoke,
+ disabled: !!apiKey.revoked_at,
},
{
icon: ,
label: t("actions.delete"),
onClick: handleDelete,
+ disabled: !apiKey.revoked_at,
},
],
},
diff --git a/packages/core/js-sdk/src/admin/api-key.ts b/packages/core/js-sdk/src/admin/api-key.ts
index 7cae5ed2b8..970665fe92 100644
--- a/packages/core/js-sdk/src/admin/api-key.ts
+++ b/packages/core/js-sdk/src/admin/api-key.ts
@@ -38,9 +38,9 @@ export class ApiKey {
async revoke(id: string, headers?: ClientHeaders) {
return await this.client.fetch(
- `/admin/api-keys/${id}`,
+ `/admin/api-keys/${id}/revoke`,
{
- method: "DELETE",
+ method: "POST",
headers,
}
)
diff --git a/packages/modules/api-key/integration-tests/__tests__/api-key-module-service.spec.ts b/packages/modules/api-key/integration-tests/__tests__/api-key-module-service.spec.ts
index 2f8026de06..571d1975d2 100644
--- a/packages/modules/api-key/integration-tests/__tests__/api-key-module-service.spec.ts
+++ b/packages/modules/api-key/integration-tests/__tests__/api-key-module-service.spec.ts
@@ -261,6 +261,12 @@ moduleIntegrationTestRunner({
createPublishableKeyFixture,
createSecretKeyFixture,
])
+
+ await service.revoke(
+ { id: [createdApiKeys[0].id, createdApiKeys[1].id] },
+ { revoked_by: "test_user" }
+ )
+
await service.deleteApiKeys([
createdApiKeys[0].id,
createdApiKeys[1].id,
@@ -269,6 +275,25 @@ moduleIntegrationTestRunner({
const apiKeysInDatabase = await service.listApiKeys()
expect(apiKeysInDatabase).toHaveLength(0)
})
+
+ it("should throw when trying to delete unrevoked api keys", async function () {
+ const createdApiKeys = await service.createApiKeys([
+ createPublishableKeyFixture,
+ createSecretKeyFixture,
+ ])
+
+ const error = await service
+ .deleteApiKeys([createdApiKeys[0].id, createdApiKeys[1].id])
+ .catch((e) => e)
+
+ expect(error.type).toEqual("not_allowed")
+ expect(error.message).toContain(
+ `Cannot delete api keys that are not revoked - `
+ )
+
+ const apiKeysInDatabase = await service.listApiKeys()
+ expect(apiKeysInDatabase).toHaveLength(2)
+ })
})
describe("authenticating with API keys", () => {
diff --git a/packages/modules/api-key/src/services/api-key-module-service.ts b/packages/modules/api-key/src/services/api-key-module-service.ts
index 3ae028f517..b62baf4a23 100644
--- a/packages/modules/api-key/src/services/api-key-module-service.ts
+++ b/packages/modules/api-key/src/services/api-key-module-service.ts
@@ -1,5 +1,3 @@
-import crypto from "crypto"
-import util from "util"
import {
ApiKeyTypes,
Context,
@@ -11,6 +9,18 @@ import {
ModuleJoinerConfig,
ModulesSdkTypes,
} from "@medusajs/types"
+import {
+ ApiKeyType,
+ InjectManager,
+ InjectTransactionManager,
+ isObject,
+ isPresent,
+ isString,
+ MedusaContext,
+ MedusaError,
+ MedusaService,
+ promiseAll,
+} from "@medusajs/utils"
import { ApiKey } from "@models"
import {
CreateApiKeyDTO,
@@ -18,17 +28,8 @@ import {
TokenDTO,
UpdateApiKeyInput,
} from "@types"
-import {
- ApiKeyType,
- InjectManager,
- InjectTransactionManager,
- isObject,
- isString,
- MedusaContext,
- MedusaError,
- MedusaService,
- promiseAll,
-} from "@medusajs/utils"
+import crypto from "crypto"
+import util from "util"
import { joinerConfig } from "../joiner-config"
const scrypt = util.promisify(crypto.scrypt)
@@ -61,6 +62,40 @@ export class ApiKeyModuleService
return joinerConfig
}
+ @InjectTransactionManager()
+ // @ts-expect-error
+ async deleteApiKeys(
+ ids: string | string[],
+ @MedusaContext() sharedContext: Context = {}
+ ) {
+ const apiKeyIds = Array.isArray(ids) ? ids : [ids]
+
+ const unrevokedApiKeys = (
+ await this.apiKeyService_.list(
+ {
+ id: ids,
+ $or: [
+ { revoked_at: { $eq: null } },
+ { revoked_at: { $gt: new Date() } },
+ ],
+ },
+ { take: null, select: ["id"] },
+ sharedContext
+ )
+ ).map((apiKey) => apiKey.id)
+
+ if (isPresent(unrevokedApiKeys)) {
+ throw new MedusaError(
+ MedusaError.Types.NOT_ALLOWED,
+ `Cannot delete api keys that are not revoked - ${unrevokedApiKeys.join(
+ ", "
+ )}`
+ )
+ }
+
+ return await super.deleteApiKeys(apiKeyIds, sharedContext)
+ }
+
//@ts-expect-error
createApiKeys(
data: ApiKeyTypes.CreateApiKeyDTO[],