Skip to content

Commit

Permalink
feat(api-key): Add the endpoints and workflows for api key module
Browse files Browse the repository at this point in the history
  • Loading branch information
sradevski committed Feb 22, 2024
1 parent 58943e8 commit a96989d
Show file tree
Hide file tree
Showing 23 changed files with 716 additions and 38 deletions.
68 changes: 68 additions & 0 deletions integration-tests/plugins/__tests__/api-key/admin/api-key.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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 create, update, revoke, and delete an api key", async () => {
const api = useApi() as any
const created = await api.post(
`/admin/api-keys`,
{
title: "Test Secret Key",
type: ApiKeyType.SECRET,
// TODO: This should be extracted from the request
created_by: "test_user",
},
adminHeaders
)

expect(created.status).toEqual(200)
expect(created.data.apiKey).toEqual(
expect.objectContaining({
id: created.data.apiKey.id,
name: "Test Secret Key",
})
)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
})

Expand All @@ -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)
Expand All @@ -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")
Expand All @@ -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(),
Expand Down
118 changes: 99 additions & 19 deletions packages/api-key/src/services/api-key-module-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
InternalModuleDeclaration,
ModuleJoinerConfig,
FindConfig,
FilterableApiKeyProps,
} from "@medusajs/types"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import { ApiKey } from "@models"
Expand All @@ -20,6 +21,8 @@ import {
MedusaContext,
MedusaError,
ModulesSdkUtils,
isObject,
isString,
} from "@medusajs/utils"

const scrypt = util.promisify(crypto.scrypt)
Expand Down Expand Up @@ -129,22 +132,31 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
return [createdApiKeys, generatedTokens]
}

update(
data: ApiKeyTypes.UpdateApiKeyDTO[],
async update(
selector: FilterableApiKeyProps,
data: Omit<ApiKeyTypes.UpdateApiKeyDTO, "id">,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO[]>
update(
data: ApiKeyTypes.UpdateApiKeyDTO,
async update(
id: string,
data: Omit<ApiKeyTypes.UpdateApiKeyDTO, "id">,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO>

async update(
data: ApiKeyTypes.UpdateApiKeyDTO[]
): Promise<ApiKeyTypes.ApiKeyDTO[]>
@InjectManager("baseRepository_")
async update(
data: ApiKeyTypes.UpdateApiKeyDTO[] | ApiKeyTypes.UpdateApiKeyDTO,
idOrSelectorOrData:
| string
| FilterableApiKeyProps
| ApiKeyTypes.UpdateApiKeyDTO[],
data?: Omit<ApiKeyTypes.UpdateApiKeyDTO, "id">,
@MedusaContext() sharedContext: Context = {}
): Promise<ApiKeyTypes.ApiKeyDTO[] | ApiKeyTypes.ApiKeyDTO> {
const updatedApiKeys = await this.update_(
Array.isArray(data) ? data : [data],
idOrSelectorOrData,
data,
sharedContext
)

Expand All @@ -154,15 +166,28 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
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<ApiKeyTypes.UpdateApiKeyDTO, "id">,
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
const updateRequest = data.map((k) => ({
const normalizedInput =
await this.normalizeUpdateInput_<ApiKeyTypes.UpdateApiKeyDTO>(
idOrSelectorOrData,
data,
sharedContext
)

const updateRequest = normalizedInput.map((k) => ({
id: k.id,
title: k.title,
}))
Expand Down Expand Up @@ -234,21 +259,30 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
}

async revoke(
data: ApiKeyTypes.RevokeApiKeyDTO[],
selector: FilterableApiKeyProps,
data: Omit<ApiKeyTypes.RevokeApiKeyDTO, "id">,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO[]>
async revoke(
data: ApiKeyTypes.RevokeApiKeyDTO,
id: string,
data: Omit<ApiKeyTypes.RevokeApiKeyDTO, "id">,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO>

async revoke(
data: ApiKeyTypes.RevokeApiKeyDTO[]
): Promise<ApiKeyTypes.ApiKeyDTO[]>
@InjectManager("baseRepository_")
async revoke(
data: ApiKeyTypes.RevokeApiKeyDTO[] | ApiKeyTypes.RevokeApiKeyDTO,
idOrSelectorOrData:
| string
| FilterableApiKeyProps
| ApiKeyTypes.RevokeApiKeyDTO[],
data?: Omit<ApiKeyTypes.RevokeApiKeyDTO, "id">,
@MedusaContext() sharedContext: Context = {}
): Promise<ApiKeyTypes.ApiKeyDTO[] | ApiKeyTypes.ApiKeyDTO> {
const revokedApiKeys = await this.revoke_(
Array.isArray(data) ? data : [data],
idOrSelectorOrData,
data,
sharedContext
)

Expand All @@ -258,17 +292,30 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
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<ApiKeyTypes.RevokeApiKeyDTO, "id">,
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
await this.validateRevokeApiKeys_(data)
const normalizedInput =
await this.normalizeUpdateInput_<ApiKeyTypes.RevokeApiKeyDTO>(
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,
Expand Down Expand Up @@ -326,6 +373,39 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
}
}

protected async normalizeUpdateInput_<T>(
idOrSelectorOrData: string | FilterableApiKeyProps | T[],
data?: Omit<T, "id">,
sharedContext: Context = {}
): Promise<T[]> {
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 = {}
Expand Down
2 changes: 2 additions & 0 deletions packages/core-flows/src/api-key/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./steps"
export * from "./workflows"
33 changes: 33 additions & 0 deletions packages/core-flows/src/api-key/steps/create-api-keys.ts
Original file line number Diff line number Diff line change
@@ -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<IApiKeyModuleService>(
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<IApiKeyModuleService>(
ModuleRegistrationName.API_KEY
)

await service.delete(createdIds)
}
)
17 changes: 17 additions & 0 deletions packages/core-flows/src/api-key/steps/delete-api-keys.ts
Original file line number Diff line number Diff line change
@@ -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<IApiKeyModuleService>(
ModuleRegistrationName.API_KEY
)

await service.delete(ids)
return new StepResponse(void 0)
},
async () => {}
)
4 changes: 4 additions & 0 deletions packages/core-flows/src/api-key/steps/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./create-api-keys"
export * from "./delete-api-keys"
export * from "./update-api-keys"
export * from "./revoke-api-keys"
Loading

0 comments on commit a96989d

Please sign in to comment.