feat(api-key): Add CRUD functionalities to the api key module

This commit is contained in:
Stevche Radevski
2024-02-21 11:19:22 +01:00
parent e0750bae40
commit c99ca5cc22
14 changed files with 812 additions and 42 deletions

View File

@@ -1 +1,14 @@
// noop
import { CreateApiKeyDTO } from "@types"
import { ApiKeyType } from "@medusajs/utils"
export const createSecretKeyFixture: CreateApiKeyDTO = {
title: "Secret key",
type: ApiKeyType.SECRET,
created_by: "test",
}
export const createPublishableKeyFixture: CreateApiKeyDTO = {
title: "Test API Key",
type: ApiKeyType.PUBLISHABLE,
created_by: "test",
}

View File

@@ -1,19 +1,266 @@
import crypto from "crypto"
import { Modules } from "@medusajs/modules-sdk"
import { IApiKeyModuleService } from "@medusajs/types"
import { ApiKeyType } from "@medusajs/utils"
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
import {
createSecretKeyFixture,
createPublishableKeyFixture,
} from "../__fixtures__"
jest.setTimeout(100000)
const mockPublishableKeyBytes = () => {
jest.spyOn(crypto, "randomBytes").mockImplementationOnce(() => {
return Buffer.from(
"44de31ebcf085fa423fc584aa854067025e937a79edb565f472404345f0f23be",
"hex"
)
})
}
const mockSecretKeyBytes = () => {
jest
.spyOn(crypto, "randomBytes")
.mockImplementationOnce(() => {
return Buffer.from(
"44de31ebcf085fa423fc584aa854067025e937a79edb565f472404345f0f23be",
"hex"
)
})
.mockImplementationOnce(() => {
return Buffer.from("44de31ebcf085fa423fc584aa8540670", "hex")
})
}
moduleIntegrationTestRunner({
moduleName: Modules.API_KEY,
testSuite: ({
MikroOrmWrapper,
service,
}: SuiteOptions<IApiKeyModuleService>) => {
afterEach(() => {
jest.restoreAllMocks()
})
describe("API Key Module Service", () => {
describe("noop", () => {
it("should run", function () {
expect(true).toBe(true)
describe("creating a publishable API key", () => {
it("should create it successfully", async function () {
mockPublishableKeyBytes()
const apiKey = await service.create(createPublishableKeyFixture)
expect(apiKey).toEqual(
expect.objectContaining({
title: "Test API Key",
type: ApiKeyType.PUBLISHABLE,
salt: "",
created_by: "test",
last_used_at: null,
revoked_by: null,
revoked_at: null,
redacted: "pk_44d***3be",
token:
"pk_44de31ebcf085fa423fc584aa854067025e937a79edb565f472404345f0f23be",
})
)
})
})
describe("creating a secret API key", () => {
it("should get created successfully", async function () {
mockSecretKeyBytes()
const apiKey = await service.create(createSecretKeyFixture)
expect(apiKey).toEqual(
expect.objectContaining({
title: "Secret key",
type: ApiKeyType.SECRET,
salt: "44de31ebcf085fa423fc584aa8540670",
created_by: "test",
last_used_at: null,
revoked_by: null,
revoked_at: null,
redacted: "sk_44d***3be",
token:
"sk_44de31ebcf085fa423fc584aa854067025e937a79edb565f472404345f0f23be",
})
)
})
it("should only allow creating one active token", async function () {
expect(
service.create([createSecretKeyFixture, createSecretKeyFixture])
).rejects.toThrow(
"You can only create one secret key at a time. You tried to create 2 secret keys."
)
await service.create(createSecretKeyFixture)
const err = await service
.create(createSecretKeyFixture)
.catch((e) => e)
expect(err.message).toEqual(
"You can only have one active secret key a time. Revoke or delete your existing key before creating a new one."
)
})
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.create(createSecretKeyFixture)
const err = await service
.create(createSecretKeyFixture)
.catch((e) => e)
expect(err.message).toEqual(
"You can only have one active secret key a time. Revoke or delete your existing key before creating a new one."
)
})
})
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,
revoked_by: "test",
})
expect(revokedKey).toEqual(
expect.objectContaining({
revoked_by: "test",
revoked_at: expect.any(Date),
})
)
})
it("should not allow revoking an already revoked API key", async function () {
const firstApiKey = await service.create(createSecretKeyFixture)
await service.revoke({
id: firstApiKey.id,
revoked_by: "test",
})
const err = await service
.revoke({
id: firstApiKey.id,
revoked_by: "test2",
})
.catch((e) => e)
expect(err.message).toEqual(
`There are 1 secret keys that are already revoked.`
)
})
})
describe("updating an API key", () => {
it("should update the name successfully", async function () {
const createdApiKey = await service.create(createSecretKeyFixture)
const updatedApiKey = await service.update({
id: createdApiKey.id,
title: "New Name",
})
expect(updatedApiKey.title).toEqual("New Name")
})
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,
title: createdApiKey.title,
revoked_by: "test",
revoked_at: new Date(),
last_used_at: new Date(),
})
// These should not be returned on an update
createdApiKey.token = ""
createdApiKey.salt = ""
expect(createdApiKey).toEqual(updatedApiKey)
})
})
describe("deleting API keys", () => {
it("should successfully delete existing api keys", async function () {
const createdApiKeys = await service.create([
createPublishableKeyFixture,
createSecretKeyFixture,
])
await service.delete([createdApiKeys[0].id, createdApiKeys[1].id])
const apiKeysInDatabase = await service.list()
expect(apiKeysInDatabase).toHaveLength(0)
})
})
describe("retrieving API keys", () => {
it("should successfully return all existing api keys", async function () {
await service.create([
createPublishableKeyFixture,
createSecretKeyFixture,
])
const apiKeysInDatabase = await service.list()
expect(apiKeysInDatabase).toHaveLength(2)
})
it("should not return the token and salt for secret keys when listing", async function () {
await service.create([createSecretKeyFixture])
const apiKeysInDatabase = await service.list()
expect(apiKeysInDatabase).toHaveLength(1)
expect(apiKeysInDatabase[0].token).toBeFalsy()
expect(apiKeysInDatabase[0].salt).toBeFalsy()
})
it("should return the token for publishable keys when listing", async function () {
await service.create([createPublishableKeyFixture])
const apiKeysInDatabase = await service.list()
expect(apiKeysInDatabase).toHaveLength(1)
expect(apiKeysInDatabase[0].token).toBeTruthy()
expect(apiKeysInDatabase[0].salt).toBeFalsy()
})
it("should not return the token and salt for secret keys when listing and counting", async function () {
await service.create([createSecretKeyFixture])
const [apiKeysInDatabase] = await service.listAndCount()
expect(apiKeysInDatabase).toHaveLength(1)
expect(apiKeysInDatabase[0].token).toBeFalsy()
expect(apiKeysInDatabase[0].salt).toBeFalsy()
})
it("should return the token for publishable keys when listing and counting", async function () {
await service.create([createPublishableKeyFixture])
const [apiKeysInDatabase] = await service.listAndCount()
expect(apiKeysInDatabase).toHaveLength(1)
expect(apiKeysInDatabase[0].token).toBeTruthy()
expect(apiKeysInDatabase[0].salt).toBeFalsy()
})
it("should not return the token and salt for secret keys when retrieving", async function () {
const [createdApiKey] = await service.create([createSecretKeyFixture])
const apiKeyInDatabase = await service.retrieve(createdApiKey.id)
expect(apiKeyInDatabase.token).toBeFalsy()
expect(apiKeyInDatabase.salt).toBeFalsy()
})
it("should return the token for publishable keys when retrieving", async function () {
const [createdApiKey] = await service.create([
createPublishableKeyFixture,
])
const apiKeyInDatabase = await service.retrieve(createdApiKey.id)
expect(apiKeyInDatabase.token).toBeTruthy()
expect(apiKeyInDatabase.salt).toBeFalsy()
})
})
})

View File

@@ -0,0 +1,140 @@
{
"namespaces": [
"public"
],
"name": "public",
"tables": [
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"token": {
"name": "token",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"salt": {
"name": "salt",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"redacted": {
"name": "redacted",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"title": {
"name": "title",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"type": {
"name": "type",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"last_used_at": {
"name": "last_used_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"mappedType": "datetime"
},
"created_by": {
"name": "created_by",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"revoked_by": {
"name": "revoked_by",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"revoked_at": {
"name": "revoked_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"mappedType": "datetime"
}
},
"name": "api_key",
"schema": "public",
"indexes": [
{
"keyName": "IDX_api_key_type",
"columnNames": [
"type"
],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_api_key_type\" ON \"api_key\" (type)"
},
{
"keyName": "api_key_pkey",
"columnNames": [
"id"
],
"composite": false,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {}
}
]
}

View File

@@ -0,0 +1,12 @@
import { Migration } from "@mikro-orm/migrations"
export class InitialSetup20240220155605 extends Migration {
async up(): Promise<void> {
this.addSql(
'create table if not exists "api_key" ("id" text not null, "token" text not null, "salt" text not null, "redacted" text not null, "title" text not null, "type" text not null, "last_used_at" timestamptz null, "created_by" text not null, "created_at" timestamptz not null default now(), "revoked_by" text null, "revoked_at" timestamptz null, constraint "api_key_pkey" primary key ("id"));'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_api_key_type" ON "api_key" (type);'
)
}
}

View File

@@ -1,4 +1,7 @@
import { generateEntityId } from "@medusajs/utils"
import {
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/utils"
import {
BeforeCreate,
@@ -6,14 +9,45 @@ import {
OnInit,
PrimaryKey,
Property,
Enum,
} from "@mikro-orm/core"
// TODO:
const TypeIndex = createPsqlIndexStatementHelper({
tableName: "api_key",
columns: "type",
})
@Entity()
export default class ApiKey {
@PrimaryKey({ columnType: "text" })
id: string
@Property({ columnType: "text" })
token: string
@Property({ columnType: "text" })
salt: string
@Property({ columnType: "text" })
redacted: string
@Property({ columnType: "text" })
title: string
@Property({ columnType: "text" })
@Enum({ items: ["publishable", "secret"] })
@TypeIndex.MikroORMIndex()
type: "publishable" | "secret"
@Property({
columnType: "timestamptz",
nullable: true,
})
last_used_at: Date | null = null
@Property({ columnType: "text" })
created_by: string
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
@@ -21,13 +55,14 @@ export default class ApiKey {
})
created_at: Date
@Property({ columnType: "text", nullable: true })
revoked_by: string | null = null
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
nullable: true,
})
updated_at: Date
revoked_at: Date | null = null
@BeforeCreate()
onCreate() {

View File

@@ -1,3 +1,5 @@
import crypto from "crypto"
import util from "util"
import {
Context,
DAL,
@@ -6,16 +8,21 @@ import {
ModulesSdkTypes,
InternalModuleDeclaration,
ModuleJoinerConfig,
FindConfig,
} from "@medusajs/types"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import { ApiKey } from "@models"
import { CreateApiKeyDTO, TokenDTO } from "@types"
import {
ApiKeyType,
InjectManager,
InjectTransactionManager,
MedusaContext,
MedusaError,
ModulesSdkUtils,
} from "@medusajs/utils"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import { ApiKey } from "@models"
const scrypt = util.promisify(crypto.scrypt)
const generateMethodForModels = []
@@ -65,28 +72,60 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
data: ApiKeyTypes.CreateApiKeyDTO | ApiKeyTypes.CreateApiKeyDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<ApiKeyTypes.ApiKeyDTO | ApiKeyTypes.ApiKeyDTO[]> {
const createdApiKeys = await this.create_(data, sharedContext)
const [createdApiKeys, generatedTokens] = await this.create_(
Array.isArray(data) ? data : [data],
sharedContext
)
return await this.baseRepository_.serialize<
ApiKeyTypes.ApiKeyDTO | ApiKeyTypes.ApiKeyDTO[]
const serializedResponse = await this.baseRepository_.serialize<
ApiKeyTypes.ApiKeyDTO[]
>(createdApiKeys, {
populate: true,
})
// When creating we want to return the raw token, as this will be the only time the user will be able to take note of it for future use.
const responseWithRawToken = serializedResponse.map((key) => ({
...key,
token:
generatedTokens.find((t) => t.hashedToken === key.token)?.rawToken ??
key.token,
}))
return Array.isArray(data) ? responseWithRawToken : responseWithRawToken[0]
}
@InjectTransactionManager("baseRepository_")
protected async create_(
data: ApiKeyTypes.CreateApiKeyDTO | ApiKeyTypes.CreateApiKeyDTO[],
data: ApiKeyTypes.CreateApiKeyDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity | TEntity[]> {
const data_ = Array.isArray(data) ? data : [data]
): Promise<[TEntity[], TokenDTO[]]> {
await this.validateCreateApiKeys(data, sharedContext)
const normalizedInput: CreateApiKeyDTO[] = []
const generatedTokens: TokenDTO[] = []
for (const key of data) {
let tokenData: TokenDTO
if (key.type === ApiKeyType.PUBLISHABLE) {
tokenData = ApiKeyModuleService.generatePublishableKey()
} else {
tokenData = await ApiKeyModuleService.generateSecretKey()
}
generatedTokens.push(tokenData)
normalizedInput.push({
...key,
token: tokenData.hashedToken,
salt: tokenData.salt,
redacted: tokenData.redacted,
})
}
const createdApiKeys = await this.apiKeyService_.create(
data_,
normalizedInput,
sharedContext
)
return Array.isArray(data) ? createdApiKeys : createdApiKeys[0]
return [createdApiKeys, generatedTokens]
}
update(
@@ -103,31 +142,148 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
data: ApiKeyTypes.UpdateApiKeyDTO[] | ApiKeyTypes.UpdateApiKeyDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<ApiKeyTypes.ApiKeyDTO[] | ApiKeyTypes.ApiKeyDTO> {
const updatedApiKeys = await this.update_(data, sharedContext)
const updatedApiKeys = await this.update_(
Array.isArray(data) ? data : [data],
sharedContext
)
return await this.baseRepository_.serialize<
ApiKeyTypes.ApiKeyDTO | ApiKeyTypes.ApiKeyDTO[]
>(updatedApiKeys, {
const serializedResponse = await this.baseRepository_.serialize<
ApiKeyTypes.ApiKeyDTO[]
>(updatedApiKeys.map(omitToken), {
populate: true,
})
return Array.isArray(data) ? serializedResponse : serializedResponse[0]
}
@InjectTransactionManager("baseRepository_")
protected async update_(
data: ApiKeyTypes.UpdateApiKeyDTO[] | ApiKeyTypes.UpdateApiKeyDTO,
data: ApiKeyTypes.UpdateApiKeyDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[] | TEntity> {
return []
): Promise<TEntity[]> {
const updateRequest = data.map((k) => ({
id: k.id,
title: k.title,
}))
const updatedApiKeys = await this.apiKeyService_.update(
updateRequest,
sharedContext
)
return updatedApiKeys
}
@InjectManager("baseRepository_")
async retrieve(
id: string,
config?: FindConfig<ApiKeyTypes.ApiKeyDTO>,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO> {
const apiKey = await this.apiKeyService_.retrieve(id, config, sharedContext)
return await this.baseRepository_.serialize<ApiKeyTypes.ApiKeyDTO>(
omitToken(apiKey),
{
populate: true,
}
)
}
@InjectManager("baseRepository_")
async list(
filters?: ApiKeyTypes.FilterableApiKeyProps,
config?: FindConfig<ApiKeyTypes.ApiKeyDTO>,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO[]> {
const apiKeys = await this.apiKeyService_.list(
filters,
config,
sharedContext
)
return this.baseRepository_.serialize<ApiKeyTypes.ApiKeyDTO[]>(
apiKeys.map(omitToken),
{
populate: true,
}
)
}
@InjectManager("baseRepository_")
async listAndCount(
filters?: ApiKeyTypes.FilterableApiKeyProps,
config?: FindConfig<ApiKeyTypes.ApiKeyDTO>,
sharedContext?: Context
): Promise<[ApiKeyTypes.ApiKeyDTO[], number]> {
const result = await this.apiKeyService_.listAndCount(
filters,
config,
sharedContext
)
const withoutToken = result[0].map(omitToken)
const count = result[1]
return [
await this.baseRepository_.serialize<ApiKeyTypes.ApiKeyDTO[]>(
withoutToken,
{
populate: true,
}
),
count,
]
}
async revoke(
data: ApiKeyTypes.RevokeApiKeyDTO[],
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO[]>
async revoke(
data: ApiKeyTypes.RevokeApiKeyDTO,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO>
@InjectManager("baseRepository_")
async revoke(
data: ApiKeyTypes.RevokeApiKeyDTO[] | ApiKeyTypes.RevokeApiKeyDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<ApiKeyTypes.ApiKeyDTO[] | ApiKeyTypes.ApiKeyDTO> {
const revokedApiKeys = await this.revoke_(
Array.isArray(data) ? data : [data],
sharedContext
)
const serializedResponse = await this.baseRepository_.serialize<
ApiKeyTypes.ApiKeyDTO[]
>(revokedApiKeys.map(omitToken), {
populate: true,
})
return Array.isArray(data) ? serializedResponse : serializedResponse[0]
}
@InjectTransactionManager("baseRepository_")
async revoke(
id: string,
async revoke_(
data: ApiKeyTypes.RevokeApiKeyDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
return
): Promise<TEntity[]> {
await this.validateRevokeApiKeys(data)
const updateRequest = data.map((k) => ({
id: k.id,
revoked_at: new Date(),
revoked_by: k.revoked_by,
}))
const revokedApiKeys = await this.apiKeyService_.update(
updateRequest,
sharedContext
)
return revokedApiKeys
}
// TODO: Implement
@InjectTransactionManager("baseRepository_")
authenticate(
id: string,
@@ -135,4 +291,108 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
): Promise<boolean> {
return Promise.resolve(false)
}
protected async validateCreateApiKeys(
data: ApiKeyTypes.CreateApiKeyDTO[],
sharedContext: Context = {}
): Promise<void> {
if (!data.length) {
return
}
// There can only be 2 secret keys at most, and one has to be with a revoked_at date set, so only 1 can be newly created.
const secretKeysToCreate = data.filter((k) => k.type === ApiKeyType.SECRET)
if (secretKeysToCreate.length > 1) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`You can only create one secret key at a time. You tried to create ${secretKeysToCreate.length} secret keys.`
)
}
// There already is a key that is not set to expire/or it hasn't expired
const dbSecretKeys = await this.apiKeyService_.list(
{
type: ApiKeyType.SECRET,
revoked_at: null,
},
{},
sharedContext
)
if (dbSecretKeys.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`You can only have one active secret key a time. Revoke or delete your existing key before creating a new one.`
)
}
}
protected async validateRevokeApiKeys(
data: ApiKeyTypes.RevokeApiKeyDTO[],
sharedContext: Context = {}
): Promise<void> {
if (!data.length) {
return
}
if (data.some((k) => !k.revoked_by)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`You must provide a revoked_by field when revoking a key.`
)
}
const revokedApiKeys = await this.apiKeyService_.list(
{
id: data.map((k) => k.id),
type: ApiKeyType.SECRET,
revoked_at: { $ne: null },
},
{},
sharedContext
)
if (revokedApiKeys.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`There are ${revokedApiKeys.length} secret keys that are already revoked.`
)
}
}
// These are public keys, so there is no point hashing them.
protected static generatePublishableKey(): TokenDTO {
const token = "pk_" + crypto.randomBytes(32).toString("hex")
return {
rawToken: token,
hashedToken: token,
salt: "",
redacted: redactKey(token),
}
}
protected static async generateSecretKey(): Promise<TokenDTO> {
const token = "sk_" + crypto.randomBytes(32).toString("hex")
const salt = crypto.randomBytes(16).toString("hex")
const hashed = ((await scrypt(token, salt, 64)) as Buffer).toString("hex")
return {
rawToken: token,
hashedToken: hashed,
salt,
redacted: redactKey(token),
}
}
}
// We are mutating the object here as what microORM relies on non-enumerable fields for serialization, among other things.
const omitToken = (key: ApiKey): ApiKey => {
key.token = key.type === ApiKeyType.SECRET ? "" : key.token
key.salt = ""
return key
}
const redactKey = (key: string): string => {
return [key.slice(0, 6), key.slice(-3)].join("***")
}

View File

@@ -1,6 +1,23 @@
import { ApiKeyType } from "@medusajs/types"
import { IEventBusModuleService, Logger } from "@medusajs/types"
export type InitializeModuleInjectableDependencies = {
logger?: Logger
eventBusService?: IEventBusModuleService
}
export type CreateApiKeyDTO = {
token: string
salt: string
redacted: string
title: string
type: ApiKeyType
created_by: string
}
export type TokenDTO = {
rawToken: string
hashedToken: string
salt: string
redacted: string
}

View File

@@ -1,8 +1,22 @@
import { BaseFilterable } from "../../dal"
// TODO:
export interface ApiKeyDTO {}
export type ApiKeyType = "secret" | "publishable"
export interface ApiKeyDTO {
id: string
token: string
redacted: string
title: string
type: ApiKeyType
last_used_at: Date | null
created_by: string
created_at: Date
revoked_by: string | null
revoked_at: Date | null
}
// TODO:
export interface FilterableApiKeyProps
extends BaseFilterable<FilterableApiKeyProps> {}
extends BaseFilterable<FilterableApiKeyProps> {
id?: string | string[]
title?: string | string[]
type?: ApiKeyType
}

View File

@@ -1,5 +1,18 @@
// TODO:
export interface CreateApiKeyDTO {}
import { ApiKeyType } from "../common"
// TODO:
export interface UpdateApiKeyDTO {}
export interface CreateApiKeyDTO {
title: string
type: ApiKeyType
created_by: string
// We could add revoked_at as a parameter (or expires_at that gets mapped to revoked_at internally) in order to support expiring tokens
}
export interface UpdateApiKeyDTO {
id: string
title?: string
}
export interface RevokeApiKeyDTO {
id: string
revoked_by: string
}

View File

@@ -2,7 +2,7 @@ import { IModuleService } from "../modules-sdk"
import { ApiKeyDTO, FilterableApiKeyProps } from "./common"
import { FindConfig } from "../common"
import { Context } from "../shared-context"
import { CreateApiKeyDTO, UpdateApiKeyDTO } from "./mutations"
import { CreateApiKeyDTO, RevokeApiKeyDTO, UpdateApiKeyDTO } from "./mutations"
export interface IApiKeyModuleService extends IModuleService {
/**
@@ -67,10 +67,11 @@ export interface IApiKeyModuleService extends IModuleService {
/**
* Revokes an api key
* @param id
* @param data
* @param sharedContext
*/
revoke(id: string, sharedContext?: Context): Promise<void>
revoke(data: RevokeApiKeyDTO[], sharedContext?: Context): Promise<ApiKeyDTO[]>
revoke(data: RevokeApiKeyDTO, sharedContext?: Context): Promise<ApiKeyDTO>
/**
* Check the validity of an api key

View File

@@ -0,0 +1,15 @@
/**
* @enum
*
* The API key's type.
*/
export enum ApiKeyType {
/**
* Publishable key that is tied to eg. a sales channel
*/
PUBLISHABLE = "publishable",
/**
* Secret key that allows access to the admin API
*/
SECRET = "secret",
}

View File

@@ -0,0 +1 @@
export * from "./api-key-type"

View File

@@ -11,3 +11,4 @@ export * as ProductUtils from "./product"
export * as PromotionUtils from "./promotion"
export * as SearchUtils from "./search"
export * as ShippingProfileUtils from "./shipping"
export * as ApiKeyUtils from "./api-key"

View File

@@ -19,5 +19,6 @@ export * from "./search"
export * from "./shipping"
export * from "./totals"
export * from "./totals/big-number"
export * from "./api-key"
export const MedusaModuleType = Symbol.for("MedusaModule")