diff --git a/integration-tests/plugins/__tests__/api-key/admin/api-key.spec.ts b/integration-tests/plugins/__tests__/api-key/admin/api-key.spec.ts new file mode 100644 index 0000000000..6375ee6beb --- /dev/null +++ b/integration-tests/plugins/__tests__/api-key/admin/api-key.spec.ts @@ -0,0 +1,109 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IApiKeyModuleService } from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import adminSeeder from "../../../../helpers/admin-seeder" +import { ApiKeyType } from "@medusajs/utils" + +jest.setTimeout(50000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +describe("API Keys - Admin", () => { + let dbConnection + let appContainer + let shutdownServer + let service: IApiKeyModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + service = appContainer.resolve(ModuleRegistrationName.API_KEY) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should correctly implement the entire lifecycle of an api key", async () => { + const api = useApi() as any + 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: "test", + }) + ) + // 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: "test", + }) + ) + 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) + }) +}) diff --git a/integration-tests/plugins/medusa-config.js b/integration-tests/plugins/medusa-config.js index b81d6e9d1c..b770b37691 100644 --- a/integration-tests/plugins/medusa-config.js +++ b/integration-tests/plugins/medusa-config.js @@ -102,5 +102,10 @@ module.exports = { resources: "shared", resolve: "@medusajs/region", }, + [Modules.API_KEY]: { + scope: "internal", + resources: "shared", + resolve: "@medusajs/api-key", + }, }, } diff --git a/integration-tests/plugins/package.json b/integration-tests/plugins/package.json index dca6ca503d..f29fa0f22e 100644 --- a/integration-tests/plugins/package.json +++ b/integration-tests/plugins/package.json @@ -9,6 +9,7 @@ "build": "babel src -d dist --extensions \".ts,.js\"" }, "dependencies": { + "@medusajs/api-key": "workspace:^", "@medusajs/auth": "workspace:*", "@medusajs/cache-inmemory": "workspace:*", "@medusajs/customer": "workspace:^", diff --git a/packages/api-key/integration-tests/__tests__/api-key-module-service.spec.ts b/packages/api-key/integration-tests/__tests__/api-key-module-service.spec.ts index 0b1e179601..a86002c9a4 100644 --- a/packages/api-key/integration-tests/__tests__/api-key-module-service.spec.ts +++ b/packages/api-key/integration-tests/__tests__/api-key-module-service.spec.ts @@ -105,10 +105,12 @@ moduleIntegrationTestRunner({ it("should allow for at most two tokens, where one is revoked", async function () { const firstApiKey = await service.create(createSecretKeyFixture) - await service.revoke({ - id: firstApiKey.id, - revoked_by: "test", - }) + await service.revoke( + { id: firstApiKey.id }, + { + revoked_by: "test", + } + ) await service.create(createSecretKeyFixture) const err = await service @@ -123,8 +125,7 @@ moduleIntegrationTestRunner({ describe("revoking API keys", () => { it("should have the revoked at and revoked by set when a key is revoked", async function () { const firstApiKey = await service.create(createSecretKeyFixture) - const revokedKey = await service.revoke({ - id: firstApiKey.id, + const revokedKey = await service.revoke(firstApiKey.id, { revoked_by: "test", }) @@ -148,14 +149,12 @@ moduleIntegrationTestRunner({ it("should not allow revoking an already revoked API key", async function () { const firstApiKey = await service.create(createSecretKeyFixture) - await service.revoke({ - id: firstApiKey.id, + await service.revoke(firstApiKey.id, { revoked_by: "test", }) const err = await service - .revoke({ - id: firstApiKey.id, + .revoke(firstApiKey.id, { revoked_by: "test2", }) .catch((e) => e) @@ -170,8 +169,7 @@ moduleIntegrationTestRunner({ it("should update the name successfully", async function () { const createdApiKey = await service.create(createSecretKeyFixture) - const updatedApiKey = await service.update({ - id: createdApiKey.id, + const updatedApiKey = await service.update(createdApiKey.id, { title: "New Name", }) expect(updatedApiKey.title).toEqual("New Name") @@ -180,8 +178,7 @@ moduleIntegrationTestRunner({ it("should not reflect any updates on other fields", async function () { const createdApiKey = await service.create(createSecretKeyFixture) - const updatedApiKey = await service.update({ - id: createdApiKey.id, + const updatedApiKey = await service.update(createdApiKey.id, { title: createdApiKey.title, revoked_by: "test", revoked_at: new Date(), diff --git a/packages/api-key/src/joiner-config.ts b/packages/api-key/src/joiner-config.ts index 95b4550664..559afda491 100644 --- a/packages/api-key/src/joiner-config.ts +++ b/packages/api-key/src/joiner-config.ts @@ -1,8 +1,7 @@ import { Modules } from "@medusajs/modules-sdk" import { ModuleJoinerConfig } from "@medusajs/types" import { MapToConfig } from "@medusajs/utils" - -// TODO manage the config +import ApiKey from "./models/api-key" export const LinkableKeys: Record = {} @@ -21,5 +20,10 @@ export const joinerConfig: ModuleJoinerConfig = { serviceName: Modules.API_KEY, primaryKeys: ["id"], linkableKeys: LinkableKeys, - alias: [], + alias: [ + { + name: ["api-key", "api-keys"], + args: { entity: ApiKey.name }, + }, + ], } as ModuleJoinerConfig diff --git a/packages/api-key/src/services/api-key-module-service.ts b/packages/api-key/src/services/api-key-module-service.ts index 03a12ca059..73f19acd11 100644 --- a/packages/api-key/src/services/api-key-module-service.ts +++ b/packages/api-key/src/services/api-key-module-service.ts @@ -9,6 +9,7 @@ import { InternalModuleDeclaration, ModuleJoinerConfig, FindConfig, + FilterableApiKeyProps, } from "@medusajs/types" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import { ApiKey } from "@models" @@ -20,6 +21,8 @@ import { MedusaContext, MedusaError, ModulesSdkUtils, + isObject, + isString, } from "@medusajs/utils" const scrypt = util.promisify(crypto.scrypt) @@ -129,22 +132,31 @@ export default class ApiKeyModuleService return [createdApiKeys, generatedTokens] } - update( - data: ApiKeyTypes.UpdateApiKeyDTO[], + async update( + selector: FilterableApiKeyProps, + data: Omit, sharedContext?: Context ): Promise - update( - data: ApiKeyTypes.UpdateApiKeyDTO, + async update( + id: string, + data: Omit, sharedContext?: Context ): Promise - + async update( + data: ApiKeyTypes.UpdateApiKeyDTO[] + ): Promise @InjectManager("baseRepository_") async update( - data: ApiKeyTypes.UpdateApiKeyDTO[] | ApiKeyTypes.UpdateApiKeyDTO, + idOrSelectorOrData: + | string + | FilterableApiKeyProps + | ApiKeyTypes.UpdateApiKeyDTO[], + data?: Omit, @MedusaContext() sharedContext: Context = {} ): Promise { const updatedApiKeys = await this.update_( - Array.isArray(data) ? data : [data], + idOrSelectorOrData, + data, sharedContext ) @@ -154,15 +166,28 @@ export default class ApiKeyModuleService populate: true, }) - return Array.isArray(data) ? serializedResponse : serializedResponse[0] + return isString(idOrSelectorOrData) + ? serializedResponse[0] + : serializedResponse } @InjectTransactionManager("baseRepository_") protected async update_( - data: ApiKeyTypes.UpdateApiKeyDTO[], + idOrSelectorOrData: + | string + | FilterableApiKeyProps + | ApiKeyTypes.UpdateApiKeyDTO[], + data?: Omit, @MedusaContext() sharedContext: Context = {} ): Promise { - const updateRequest = data.map((k) => ({ + const normalizedInput = + await this.normalizeUpdateInput_( + idOrSelectorOrData, + data, + sharedContext + ) + + const updateRequest = normalizedInput.map((k) => ({ id: k.id, title: k.title, })) @@ -234,21 +259,30 @@ export default class ApiKeyModuleService } async revoke( - data: ApiKeyTypes.RevokeApiKeyDTO[], + selector: FilterableApiKeyProps, + data: Omit, sharedContext?: Context ): Promise async revoke( - data: ApiKeyTypes.RevokeApiKeyDTO, + id: string, + data: Omit, sharedContext?: Context ): Promise - + async revoke( + data: ApiKeyTypes.RevokeApiKeyDTO[] + ): Promise @InjectManager("baseRepository_") async revoke( - data: ApiKeyTypes.RevokeApiKeyDTO[] | ApiKeyTypes.RevokeApiKeyDTO, + idOrSelectorOrData: + | string + | FilterableApiKeyProps + | ApiKeyTypes.RevokeApiKeyDTO[], + data?: Omit, @MedusaContext() sharedContext: Context = {} ): Promise { const revokedApiKeys = await this.revoke_( - Array.isArray(data) ? data : [data], + idOrSelectorOrData, + data, sharedContext ) @@ -258,17 +292,30 @@ export default class ApiKeyModuleService populate: true, }) - return Array.isArray(data) ? serializedResponse : serializedResponse[0] + return isString(idOrSelectorOrData) + ? serializedResponse[0] + : serializedResponse } @InjectTransactionManager("baseRepository_") async revoke_( - data: ApiKeyTypes.RevokeApiKeyDTO[], + idOrSelectorOrData: + | string + | FilterableApiKeyProps + | ApiKeyTypes.RevokeApiKeyDTO[], + data?: Omit, @MedusaContext() sharedContext: Context = {} ): Promise { - await this.validateRevokeApiKeys_(data) + const normalizedInput = + await this.normalizeUpdateInput_( + idOrSelectorOrData, + data, + sharedContext + ) - const updateRequest = data.map((k) => ({ + await this.validateRevokeApiKeys_(normalizedInput) + + const updateRequest = normalizedInput.map((k) => ({ id: k.id, revoked_at: new Date(), revoked_by: k.revoked_by, @@ -326,6 +373,39 @@ export default class ApiKeyModuleService } } + protected async normalizeUpdateInput_( + idOrSelectorOrData: string | FilterableApiKeyProps | T[], + data?: Omit, + sharedContext: Context = {} + ): Promise { + let normalizedInput: T[] = [] + if (isString(idOrSelectorOrData)) { + normalizedInput = [{ id: idOrSelectorOrData, ...data } as T] + } + + if (Array.isArray(idOrSelectorOrData)) { + normalizedInput = idOrSelectorOrData + } + + if (isObject(idOrSelectorOrData)) { + const apiKeys = await this.apiKeyService_.list( + idOrSelectorOrData, + {}, + sharedContext + ) + + normalizedInput = apiKeys.map( + (apiKey) => + ({ + id: apiKey.id, + ...data, + } as T) + ) + } + + return normalizedInput + } + protected async validateRevokeApiKeys_( data: ApiKeyTypes.RevokeApiKeyDTO[], sharedContext: Context = {} diff --git a/packages/core-flows/src/api-key/index.ts b/packages/core-flows/src/api-key/index.ts new file mode 100644 index 0000000000..68de82c9f9 --- /dev/null +++ b/packages/core-flows/src/api-key/index.ts @@ -0,0 +1,2 @@ +export * from "./steps" +export * from "./workflows" diff --git a/packages/core-flows/src/api-key/steps/create-api-keys.ts b/packages/core-flows/src/api-key/steps/create-api-keys.ts new file mode 100644 index 0000000000..8c75a965ff --- /dev/null +++ b/packages/core-flows/src/api-key/steps/create-api-keys.ts @@ -0,0 +1,33 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { CreateApiKeyDTO, IApiKeyModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type CreateApiKeysStepInput = { + apiKeysData: CreateApiKeyDTO[] +} + +export const createApiKeysStepId = "create-api-keys" +export const createApiKeysStep = createStep( + createApiKeysStepId, + async (data: CreateApiKeysStepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.API_KEY + ) + const created = await service.create(data.apiKeysData) + return new StepResponse( + created, + created.map((apiKey) => apiKey.id) + ) + }, + async (createdIds, { container }) => { + if (!createdIds?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.API_KEY + ) + + await service.delete(createdIds) + } +) diff --git a/packages/core-flows/src/api-key/steps/delete-api-keys.ts b/packages/core-flows/src/api-key/steps/delete-api-keys.ts new file mode 100644 index 0000000000..d24f38ea89 --- /dev/null +++ b/packages/core-flows/src/api-key/steps/delete-api-keys.ts @@ -0,0 +1,17 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IApiKeyModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +export const deleteApiKeysStepId = "delete-api-keys" +export const deleteApiKeysStep = createStep( + { name: deleteApiKeysStepId, noCompensation: true }, + async (ids: string[], { container }) => { + const service = container.resolve( + ModuleRegistrationName.API_KEY + ) + + await service.delete(ids) + return new StepResponse(void 0) + }, + async () => {} +) diff --git a/packages/core-flows/src/api-key/steps/index.ts b/packages/core-flows/src/api-key/steps/index.ts new file mode 100644 index 0000000000..829543dbbb --- /dev/null +++ b/packages/core-flows/src/api-key/steps/index.ts @@ -0,0 +1,4 @@ +export * from "./create-api-keys" +export * from "./delete-api-keys" +export * from "./update-api-keys" +export * from "./revoke-api-keys" diff --git a/packages/core-flows/src/api-key/steps/revoke-api-keys.ts b/packages/core-flows/src/api-key/steps/revoke-api-keys.ts new file mode 100644 index 0000000000..a234b7e522 --- /dev/null +++ b/packages/core-flows/src/api-key/steps/revoke-api-keys.ts @@ -0,0 +1,27 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + FilterableApiKeyProps, + IApiKeyModuleService, + RevokeApiKeyDTO, +} from "@medusajs/types" +import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type RevokeApiKeysStepInput = { + selector: FilterableApiKeyProps + revoke: Omit +} + +export const revokeApiKeysStepId = "revoke-api-keys" +export const revokeApiKeysStep = createStep( + { name: revokeApiKeysStepId, noCompensation: true }, + async (data: RevokeApiKeysStepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.API_KEY + ) + + const apiKeys = await service.revoke(data.selector, data.revoke) + return new StepResponse(apiKeys) + }, + async () => {} +) diff --git a/packages/core-flows/src/api-key/steps/update-api-keys.ts b/packages/core-flows/src/api-key/steps/update-api-keys.ts new file mode 100644 index 0000000000..7394a27f32 --- /dev/null +++ b/packages/core-flows/src/api-key/steps/update-api-keys.ts @@ -0,0 +1,51 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + FilterableApiKeyProps, + IApiKeyModuleService, + UpdateApiKeyDTO, +} from "@medusajs/types" +import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type UpdateApiKeysStepInput = { + selector: FilterableApiKeyProps + update: Omit +} + +export const updateApiKeysStepId = "update-api-keys" +export const updateApiKeysStep = createStep( + updateApiKeysStepId, + async (data: UpdateApiKeysStepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.API_KEY + ) + + const { selects, relations } = getSelectsAndRelationsFromObjectArray([ + data.update, + ]) + + const prevData = await service.list(data.selector, { + select: selects, + relations, + }) + + const apiKeys = await service.update(data.selector, data.update) + return new StepResponse(apiKeys, prevData) + }, + async (prevData, { container }) => { + if (!prevData?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.API_KEY + ) + + await service.update( + prevData.map((r) => ({ + id: r.id, + title: r.title, + })) + ) + } +) diff --git a/packages/core-flows/src/api-key/workflows/create-api-keys.ts b/packages/core-flows/src/api-key/workflows/create-api-keys.ts new file mode 100644 index 0000000000..d01bc7bd20 --- /dev/null +++ b/packages/core-flows/src/api-key/workflows/create-api-keys.ts @@ -0,0 +1,13 @@ +import { ApiKeyDTO, CreateApiKeyDTO } from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { createApiKeysStep } from "../steps" + +type WorkflowInput = { apiKeysData: CreateApiKeyDTO[] } + +export const createApiKeysWorkflowId = "create-api-keys" +export const createApiKeysWorkflow = createWorkflow( + createApiKeysWorkflowId, + (input: WorkflowData): WorkflowData => { + return createApiKeysStep(input) + } +) diff --git a/packages/core-flows/src/api-key/workflows/delete-api-keys.ts b/packages/core-flows/src/api-key/workflows/delete-api-keys.ts new file mode 100644 index 0000000000..a94df6e224 --- /dev/null +++ b/packages/core-flows/src/api-key/workflows/delete-api-keys.ts @@ -0,0 +1,12 @@ +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { deleteApiKeysStep } from "../steps" + +type WorkflowInput = { ids: string[] } + +export const deleteApiKeysWorkflowId = "delete-api-keys" +export const deleteApiKeysWorkflow = createWorkflow( + deleteApiKeysWorkflowId, + (input: WorkflowData): WorkflowData => { + return deleteApiKeysStep(input.ids) + } +) diff --git a/packages/core-flows/src/api-key/workflows/index.ts b/packages/core-flows/src/api-key/workflows/index.ts new file mode 100644 index 0000000000..829543dbbb --- /dev/null +++ b/packages/core-flows/src/api-key/workflows/index.ts @@ -0,0 +1,4 @@ +export * from "./create-api-keys" +export * from "./delete-api-keys" +export * from "./update-api-keys" +export * from "./revoke-api-keys" diff --git a/packages/core-flows/src/api-key/workflows/revoke-api-keys.ts b/packages/core-flows/src/api-key/workflows/revoke-api-keys.ts new file mode 100644 index 0000000000..6c1d4be0da --- /dev/null +++ b/packages/core-flows/src/api-key/workflows/revoke-api-keys.ts @@ -0,0 +1,22 @@ +import { + ApiKeyDTO, + FilterableApiKeyProps, + RevokeApiKeyDTO, +} from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { revokeApiKeysStep } from "../steps" + +type RevokeApiKeysStepInput = { + selector: FilterableApiKeyProps + revoke: Omit +} + +type WorkflowInput = RevokeApiKeysStepInput + +export const revokeApiKeysWorkflowId = "revoke-api-keys" +export const revokeApiKeysWorkflow = createWorkflow( + revokeApiKeysWorkflowId, + (input: WorkflowData): WorkflowData => { + return revokeApiKeysStep(input) + } +) diff --git a/packages/core-flows/src/api-key/workflows/update-api-keys.ts b/packages/core-flows/src/api-key/workflows/update-api-keys.ts new file mode 100644 index 0000000000..452658a7e6 --- /dev/null +++ b/packages/core-flows/src/api-key/workflows/update-api-keys.ts @@ -0,0 +1,22 @@ +import { + ApiKeyDTO, + FilterableApiKeyProps, + UpdateApiKeyDTO, +} from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { updateApiKeysStep } from "../steps" + +type UpdateApiKeysStepInput = { + selector: FilterableApiKeyProps + update: Omit +} + +type WorkflowInput = UpdateApiKeysStepInput + +export const updateApiKeysWorkflowId = "update-api-keys" +export const updateApiKeysWorkflow = createWorkflow( + updateApiKeysWorkflowId, + (input: WorkflowData): WorkflowData => { + return updateApiKeysStep(input) + } +) diff --git a/packages/core-flows/src/index.ts b/packages/core-flows/src/index.ts index 2c8bb09c39..aa4e2ef320 100644 --- a/packages/core-flows/src/index.ts +++ b/packages/core-flows/src/index.ts @@ -7,4 +7,4 @@ export * from "./invite" export * from "./promotion" export * from "./region" export * from "./user" - +export * from "./api-key" 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 new file mode 100644 index 0000000000..2a291cf379 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/api-keys/[id]/revoke/route.ts @@ -0,0 +1,23 @@ +import { revokeApiKeysWorkflow } from "@medusajs/core-flows" +import { RevokeApiKeyDTO } from "@medusajs/types" +import { MedusaRequest, MedusaResponse } from "../../../../../types/routing" + +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + const id = req.params.id + + const { result, errors } = await revokeApiKeysWorkflow(req.scope).run({ + input: { + selector: { id: req.params.id }, + revoke: { + revoked_by: req.auth_user?.id, + } as RevokeApiKeyDTO, + }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + res.status(200).json({ apiKey: result[0] }) +} 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 new file mode 100644 index 0000000000..4a1b644ab9 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/api-keys/[id]/route.ts @@ -0,0 +1,59 @@ +import { + deleteApiKeysWorkflow, + updateApiKeysWorkflow, +} from "@medusajs/core-flows" +import { UpdateApiKeyDTO } from "@medusajs/types" +import { remoteQueryObjectFromString } from "@medusajs/utils" +import { MedusaRequest, MedusaResponse } from "../../../../types/routing" +import { defaultAdminApiKeyFields } from "../query-config" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const remoteQuery = req.scope.resolve("remoteQuery") + + const variables = { id: req.params.id } + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "api-key", + variables, + fields: defaultAdminApiKeyFields, + }) + + const [apiKey] = await remoteQuery(queryObject) + + res.status(200).json({ apiKey }) +} + +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + const { result, errors } = await updateApiKeysWorkflow(req.scope).run({ + input: { + selector: { id: req.params.id }, + update: req.validatedBody as Omit, + }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + res.status(200).json({ apiKey: result[0] }) +} + +export const DELETE = async (req: MedusaRequest, res: MedusaResponse) => { + const id = req.params.id + + const { errors } = await deleteApiKeysWorkflow(req.scope).run({ + input: { ids: [id] }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + res.status(200).json({ + id, + object: "api-key", + deleted: true, + }) +} diff --git a/packages/medusa/src/api-v2/admin/api-keys/middlewares.ts b/packages/medusa/src/api-v2/admin/api-keys/middlewares.ts new file mode 100644 index 0000000000..e480fa1d97 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/api-keys/middlewares.ts @@ -0,0 +1,64 @@ +import { transformBody, transformQuery } from "../../../api/middlewares" +import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import * as QueryConfig from "./query-config" +import { + AdminGetApiKeysParams, + AdminGetApiKeysApiKeyParams, + AdminPostApiKeysReq, + AdminPostApiKeysApiKeyReq, + AdminRevokeApiKeysApiKeyReq, +} from "./validators" + +export const adminApiKeyRoutesMiddlewares: MiddlewareRoute[] = [ + { + matcher: "/admin/api-keys*", + // middlewares: [authenticate("admin", ["bearer", "session"])], + // TODO: Apply authentication middleware correctly once https://github.com/medusajs/medusa/pull/6447 is merged. + middlewares: [ + (req, res, next) => { + req.auth_user = { id: "test" } + next() + }, + ], + }, + { + method: ["GET"], + matcher: "/admin/api-keys", + middlewares: [ + transformQuery( + AdminGetApiKeysParams, + QueryConfig.listTransformQueryConfig + ), + ], + }, + { + method: ["GET"], + matcher: "/admin/api-keys/:id", + middlewares: [ + transformQuery( + AdminGetApiKeysApiKeyParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, + { + method: ["POST"], + matcher: "/admin/api-keys", + middlewares: [transformBody(AdminPostApiKeysReq)], + }, + { + method: ["POST"], + matcher: "/admin/api-keys/:id", + middlewares: [transformBody(AdminPostApiKeysApiKeyReq)], + }, + { + method: ["DELETE"], + matcher: "/admin/api-keys/:id", + middlewares: [], + }, + { + method: ["POST"], + matcher: "/admin/api-keys/:id/revoke", + middlewares: [transformBody(AdminRevokeApiKeysApiKeyReq)], + }, +] 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 new file mode 100644 index 0000000000..9160898416 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/api-keys/query-config.ts @@ -0,0 +1,26 @@ +export const defaultAdminApiKeyRelations = [] +export const allowedAdminApiKeyRelations = [] +export const defaultAdminApiKeyFields = [ + "id", + "title", + "token", + "redacted", + "type", + "last_used_at", + "created_at", + "created_by", + "revoked_at", + "revoked_by", +] + +export const retrieveTransformQueryConfig = { + defaultFields: defaultAdminApiKeyFields, + defaultRelations: defaultAdminApiKeyRelations, + allowedRelations: allowedAdminApiKeyRelations, + isList: false, +} + +export const listTransformQueryConfig = { + 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 new file mode 100644 index 0000000000..ce9b5b324c --- /dev/null +++ b/packages/medusa/src/api-v2/admin/api-keys/route.ts @@ -0,0 +1,49 @@ +import { createApiKeysWorkflow } from "@medusajs/core-flows" +import { CreateApiKeyDTO } from "@medusajs/types" +import { remoteQueryObjectFromString } from "@medusajs/utils" +import { MedusaRequest, MedusaResponse } from "../../../types/routing" +import { defaultAdminApiKeyFields } from "./query-config" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const remoteQuery = req.scope.resolve("remoteQuery") + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "api-key", + variables: { + filters: req.filterableFields, + order: req.listConfig.order, + skip: req.listConfig.skip, + take: req.listConfig.take, + }, + fields: defaultAdminApiKeyFields, + }) + + const { rows: apiKeys, metadata } = await remoteQuery(queryObject) + + res.json({ + apiKeys, + count: metadata.count, + offset: metadata.skip, + limit: metadata.take, + }) +} + +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + const input = [ + { + ...(req.validatedBody as Omit), + created_by: req.auth_user?.id, + } as CreateApiKeyDTO, + ] + + const { result, errors } = await createApiKeysWorkflow(req.scope).run({ + input: { apiKeysData: input }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + res.status(200).json({ apiKey: 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 new file mode 100644 index 0000000000..7e54b6aa21 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/api-keys/validators.ts @@ -0,0 +1,70 @@ +import { OperatorMap } from "@medusajs/types" +import { Type } from "class-transformer" +import { + IsArray, + IsEnum, + IsOptional, + IsString, + 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 {} +/** + * Parameters used to filter and configure the pagination of the retrieved api keys. + */ +export class AdminGetApiKeysParams extends extendedFindParamsMixin({ + limit: 50, + offset: 0, +}) { + /** + * Search parameter for api keys. + */ + @IsString({ each: true }) + @IsOptional() + id?: string | string[] + + /** + * Filter by title + */ + @IsString({ each: true }) + @IsOptional() + title?: string | string[] + + /** + * Filter by type + */ + @IsEnum(ApiKeyType, { each: true }) + @IsOptional() + type?: ApiKeyType + + // Additional filters from BaseFilterable + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => AdminGetApiKeysParams) + $and?: AdminGetApiKeysParams[] + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => AdminGetApiKeysParams) + $or?: AdminGetApiKeysParams[] +} + +export class AdminPostApiKeysReq { + @IsString() + title: string + + @IsEnum(ApiKeyType, {}) + type: ApiKeyType +} + +export class AdminPostApiKeysApiKeyReq { + @IsString() + title: string +} + +export class AdminRevokeApiKeysApiKeyReq {} + +export class AdminDeleteApiKeysApiKeyReq {} diff --git a/packages/medusa/src/api-v2/middlewares.ts b/packages/medusa/src/api-v2/middlewares.ts index 8ead6492b3..60ba673a46 100644 --- a/packages/medusa/src/api-v2/middlewares.ts +++ b/packages/medusa/src/api-v2/middlewares.ts @@ -1,4 +1,5 @@ import { MiddlewaresConfig } from "../loaders/helpers/routing/types" +import { adminApiKeyRoutesMiddlewares } from "./admin/api-keys/middlewares" import { adminCampaignRoutesMiddlewares } from "./admin/campaigns/middlewares" import { adminCustomerGroupRoutesMiddlewares } from "./admin/customer-groups/middlewares" import { adminCustomerRoutesMiddlewares } from "./admin/customers/middlewares" @@ -27,5 +28,6 @@ export const config: MiddlewaresConfig = { ...adminRegionRoutesMiddlewares, ...adminUserRoutesMiddlewares, ...adminInviteRoutesMiddlewares, + ...adminApiKeyRoutesMiddlewares, ], } diff --git a/packages/types/src/api-key/service.ts b/packages/types/src/api-key/service.ts index 4447c1e3c1..0b4c9fe992 100644 --- a/packages/types/src/api-key/service.ts +++ b/packages/types/src/api-key/service.ts @@ -15,11 +15,31 @@ export interface IApiKeyModuleService extends IModuleService { /** * Update an api key + * @param selector * @param data * @param sharedContext */ - update(data: UpdateApiKeyDTO[], sharedContext?: Context): Promise - update(data: UpdateApiKeyDTO, sharedContext?: Context): Promise + update( + selector: FilterableApiKeyProps, + data: Omit, + sharedContext?: Context + ): Promise + /** + * Update an api key + * @param id + * @param data + * @param sharedContext + */ + update( + id: string, + data: Omit, + sharedContext?: Context + ): Promise + /** + * Update an api key + * @param data + */ + update(data: UpdateApiKeyDTO[]): Promise /** * Delete an api key @@ -67,11 +87,31 @@ export interface IApiKeyModuleService extends IModuleService { /** * Revokes an api key + * @param selector * @param data * @param sharedContext */ - revoke(data: RevokeApiKeyDTO[], sharedContext?: Context): Promise - revoke(data: RevokeApiKeyDTO, sharedContext?: Context): Promise + revoke( + selector: FilterableApiKeyProps, + data: Omit, + sharedContext?: Context + ): Promise + /** + * Revokes an api key + * @param id + * @param data + * @param sharedContext + */ + revoke( + id: string, + data: Omit, + sharedContext?: Context + ): Promise + /** + * Revokes an api key + * @param data + */ + revoke(data: RevokeApiKeyDTO[]): Promise /** * Check the validity of an api key diff --git a/yarn.lock b/yarn.lock index cf640f8a84..63b4c1eb3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7898,7 +7898,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/api-key@workspace:packages/api-key": +"@medusajs/api-key@workspace:^, @medusajs/api-key@workspace:packages/api-key": version: 0.0.0-use.local resolution: "@medusajs/api-key@workspace:packages/api-key" dependencies: @@ -31660,6 +31660,7 @@ __metadata: "@babel/cli": ^7.12.10 "@babel/core": ^7.12.10 "@babel/node": ^7.12.10 + "@medusajs/api-key": "workspace:^" "@medusajs/auth": "workspace:*" "@medusajs/cache-inmemory": "workspace:*" "@medusajs/customer": "workspace:^"