feat(api-key,js-sdk,dashboard): allow deleting api keys only once its revoked (#9118)
what: - module only deletes api keys once its revoked - disables ui elements https://github.com/user-attachments/assets/437821ae-497e-4b59-b02c-4a6ff36e6a30 RESOLVES CC-106 RESOLVES CC-105 RESOLVES CC-104
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { ApiKeyType, ContainerRegistrationKeys } from "@medusajs/utils"
|
import { ApiKeyType } from "@medusajs/utils"
|
||||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
import {
|
import {
|
||||||
adminHeaders,
|
adminHeaders,
|
||||||
@@ -372,9 +372,7 @@ medusaIntegrationTestRunner({
|
|||||||
it("should detach sales channels from a publishable API key on delete", async () => {
|
it("should detach sales channels from a publishable API key on delete", async () => {
|
||||||
const salesChannelRes = await api.post(
|
const salesChannelRes = await api.post(
|
||||||
`/admin/sales-channels`,
|
`/admin/sales-channels`,
|
||||||
{
|
{ name: "Test Sales Channel" },
|
||||||
name: "Test Sales Channel",
|
|
||||||
},
|
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -393,9 +391,7 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
const keyWithChannelsRes = await api.post(
|
const keyWithChannelsRes = await api.post(
|
||||||
`/admin/api-keys/${api_key.id}/sales-channels`,
|
`/admin/api-keys/${api_key.id}/sales-channels`,
|
||||||
{
|
{ add: [sales_channel.id] },
|
||||||
add: [sales_channel.id],
|
|
||||||
},
|
|
||||||
adminHeaders
|
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)
|
await api.delete(`/admin/api-keys/${api_key.id}`, adminHeaders)
|
||||||
|
|
||||||
const deletedApiKeys = await api.get(
|
const deletedApiKeys = await api.get(
|
||||||
|
|||||||
@@ -129,6 +129,12 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
describe("DELETE /admin/api-keys/:id", () => {
|
describe("DELETE /admin/api-keys/:id", () => {
|
||||||
it("delete a publishable key", async () => {
|
it("delete a publishable key", async () => {
|
||||||
|
await api.post(
|
||||||
|
`/admin/api-keys/${pubKey1.id}/revoke`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
const response1 = await api.delete(
|
const response1 = await api.delete(
|
||||||
`/admin/api-keys/${pubKey1.id}`,
|
`/admin/api-keys/${pubKey1.id}`,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
|
|||||||
+7
-2
@@ -12,7 +12,10 @@ import {
|
|||||||
} from "@medusajs/ui"
|
} from "@medusajs/ui"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useNavigate } from "react-router-dom"
|
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 { Skeleton } from "../../../../../components/common/skeleton"
|
||||||
import { UserLink } from "../../../../../components/common/user-link"
|
import { UserLink } from "../../../../../components/common/user-link"
|
||||||
import {
|
import {
|
||||||
@@ -97,11 +100,12 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const dangerousActions = [
|
const dangerousActions: Action[] = [
|
||||||
{
|
{
|
||||||
icon: <Trash />,
|
icon: <Trash />,
|
||||||
label: t("actions.delete"),
|
label: t("actions.delete"),
|
||||||
onClick: handleDelete,
|
onClick: handleDelete,
|
||||||
|
disabled: !apiKey.revoked_at,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -110,6 +114,7 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
|||||||
icon: <XCircle />,
|
icon: <XCircle />,
|
||||||
label: t("apiKeyManagement.actions.revoke"),
|
label: t("apiKeyManagement.actions.revoke"),
|
||||||
onClick: handleRevoke,
|
onClick: handleRevoke,
|
||||||
|
disabled: !!apiKey.revoked_at,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
@@ -107,11 +107,13 @@ export const ApiKeyRowActions = ({
|
|||||||
icon: <XCircle />,
|
icon: <XCircle />,
|
||||||
label: t("apiKeyManagement.actions.revoke"),
|
label: t("apiKeyManagement.actions.revoke"),
|
||||||
onClick: handleRevoke,
|
onClick: handleRevoke,
|
||||||
|
disabled: !!apiKey.revoked_at,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Trash />,
|
icon: <Trash />,
|
||||||
label: t("actions.delete"),
|
label: t("actions.delete"),
|
||||||
onClick: handleDelete,
|
onClick: handleDelete,
|
||||||
|
disabled: !apiKey.revoked_at,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ export class ApiKey {
|
|||||||
|
|
||||||
async revoke(id: string, headers?: ClientHeaders) {
|
async revoke(id: string, headers?: ClientHeaders) {
|
||||||
return await this.client.fetch<HttpTypes.AdminApiKeyResponse>(
|
return await this.client.fetch<HttpTypes.AdminApiKeyResponse>(
|
||||||
`/admin/api-keys/${id}`,
|
`/admin/api-keys/${id}/revoke`,
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -261,6 +261,12 @@ moduleIntegrationTestRunner<IApiKeyModuleService>({
|
|||||||
createPublishableKeyFixture,
|
createPublishableKeyFixture,
|
||||||
createSecretKeyFixture,
|
createSecretKeyFixture,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
await service.revoke(
|
||||||
|
{ id: [createdApiKeys[0].id, createdApiKeys[1].id] },
|
||||||
|
{ revoked_by: "test_user" }
|
||||||
|
)
|
||||||
|
|
||||||
await service.deleteApiKeys([
|
await service.deleteApiKeys([
|
||||||
createdApiKeys[0].id,
|
createdApiKeys[0].id,
|
||||||
createdApiKeys[1].id,
|
createdApiKeys[1].id,
|
||||||
@@ -269,6 +275,25 @@ moduleIntegrationTestRunner<IApiKeyModuleService>({
|
|||||||
const apiKeysInDatabase = await service.listApiKeys()
|
const apiKeysInDatabase = await service.listApiKeys()
|
||||||
expect(apiKeysInDatabase).toHaveLength(0)
|
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", () => {
|
describe("authenticating with API keys", () => {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import crypto from "crypto"
|
|
||||||
import util from "util"
|
|
||||||
import {
|
import {
|
||||||
ApiKeyTypes,
|
ApiKeyTypes,
|
||||||
Context,
|
Context,
|
||||||
@@ -11,6 +9,18 @@ import {
|
|||||||
ModuleJoinerConfig,
|
ModuleJoinerConfig,
|
||||||
ModulesSdkTypes,
|
ModulesSdkTypes,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
ApiKeyType,
|
||||||
|
InjectManager,
|
||||||
|
InjectTransactionManager,
|
||||||
|
isObject,
|
||||||
|
isPresent,
|
||||||
|
isString,
|
||||||
|
MedusaContext,
|
||||||
|
MedusaError,
|
||||||
|
MedusaService,
|
||||||
|
promiseAll,
|
||||||
|
} from "@medusajs/utils"
|
||||||
import { ApiKey } from "@models"
|
import { ApiKey } from "@models"
|
||||||
import {
|
import {
|
||||||
CreateApiKeyDTO,
|
CreateApiKeyDTO,
|
||||||
@@ -18,17 +28,8 @@ import {
|
|||||||
TokenDTO,
|
TokenDTO,
|
||||||
UpdateApiKeyInput,
|
UpdateApiKeyInput,
|
||||||
} from "@types"
|
} from "@types"
|
||||||
import {
|
import crypto from "crypto"
|
||||||
ApiKeyType,
|
import util from "util"
|
||||||
InjectManager,
|
|
||||||
InjectTransactionManager,
|
|
||||||
isObject,
|
|
||||||
isString,
|
|
||||||
MedusaContext,
|
|
||||||
MedusaError,
|
|
||||||
MedusaService,
|
|
||||||
promiseAll,
|
|
||||||
} from "@medusajs/utils"
|
|
||||||
import { joinerConfig } from "../joiner-config"
|
import { joinerConfig } from "../joiner-config"
|
||||||
|
|
||||||
const scrypt = util.promisify(crypto.scrypt)
|
const scrypt = util.promisify(crypto.scrypt)
|
||||||
@@ -61,6 +62,40 @@ export class ApiKeyModuleService
|
|||||||
return joinerConfig
|
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
|
//@ts-expect-error
|
||||||
createApiKeys(
|
createApiKeys(
|
||||||
data: ApiKeyTypes.CreateApiKeyDTO[],
|
data: ApiKeyTypes.CreateApiKeyDTO[],
|
||||||
|
|||||||
Reference in New Issue
Block a user