feat(api-key): Allow revoking in the future, and enforce the secret key (#6484)

Since there is quite a bit of code here already, I'll do the middleware changes in a separate PR
This commit is contained in:
Stevche Radevski authored and GitHub committed 2024-02-27 10:37:32 +00:00
1 parent ca463ae9a9
commit 690e8c2e09
13 files changed
+357 -120

No files matched your search

@@ -108,4 +108,29 @@ describe("API Keys - Admin", () => {
expect(deleted.status).toEqual(200) expect(deleted.status).toEqual(200)
expect(listedApiKeys.data.apiKeys).toHaveLength(0) expect(listedApiKeys.data.apiKeys).toHaveLength(0)
}) })
it.skip("can use a secret api key for authentication", async () => {
const api = useApi() as any
const created = await api.post(
`/admin/api-keys`,
{
title: "Test Secret Key",
type: ApiKeyType.SECRET,
},
adminHeaders
)
const createdRegion = await api.post(
`/admin/regions`,
{
name: "Test Region",
currency_code: "usd",
countries: ["us", "ca"],
},
{ headers: { Authorization: `Bearer ${created.token}` } }
)
expect(createdRegion.status).toEqual(200)
expect(createdRegion.data.region.name).toEqual("Test Region")
})
}) })
@@ -137,6 +137,27 @@ moduleIntegrationTestRunner({
) )
}) })
it("should be able to revoke a key in the future", async function () {
const now = Date.parse("2021-01-01T00:00:00Z")
const hourInSec = 3600
jest.useFakeTimers().setSystemTime(now)
const createdKey = await service.create(createSecretKeyFixture)
const revokedKey = await service.revoke(createdKey.id, {
revoked_by: "test",
revoke_in: hourInSec,
})
expect(revokedKey).toEqual(
expect.objectContaining({
revoked_by: "test",
revoked_at: new Date(now + hourInSec * 1000),
})
)
jest.useRealTimers()
})
it("should do nothing if the revokal list is empty", async function () { it("should do nothing if the revokal list is empty", async function () {
const firstApiKey = await service.create(createSecretKeyFixture) const firstApiKey = await service.create(createSecretKeyFixture)
let revokedKeys = await service.revoke([]) let revokedKeys = await service.revoke([])
@@ -204,6 +225,54 @@ moduleIntegrationTestRunner({
}) })
}) })
describe("authenticating with API keys", () => {
it("should authenticate a secret key successfully", async function () {
const createdApiKey = await service.create(createSecretKeyFixture)
const authenticated = await service.authenticate(createdApiKey.token)
expect(authenticated).toBeTruthy()
expect(authenticated.title).toEqual(createSecretKeyFixture.title)
})
it("should authenticate with a token to be revoked in the future", async function () {
const createdApiKey = await service.create(createSecretKeyFixture)
// We simulate setting the revoked_at in the future here
jest.useFakeTimers().setSystemTime(new Date().setFullYear(3000))
await service.revoke(createdApiKey.id, {
revoked_by: "test",
})
jest.useRealTimers()
const authenticated = await service.authenticate(createdApiKey.token)
expect(authenticated).toBeTruthy()
expect(authenticated.title).toEqual(createdApiKey.title)
})
it("should not authenticate a publishable key", async function () {
const createdApiKey = await service.create(
createPublishableKeyFixture
)
const authenticated = await service.authenticate(createdApiKey.token)
expect(authenticated).toBeFalsy()
})
it("should not authenticate with a non-existent token", async function () {
const createdApiKey = await service.create(createSecretKeyFixture)
const authenticated = await service.authenticate("some-token")
expect(authenticated).toBeFalsy()
})
it("should not authenticate with a revoked token", async function () {
const createdApiKey = await service.create(createSecretKeyFixture)
await service.revoke(createdApiKey.id, {
revoked_by: "test",
})
const authenticated = await service.authenticate(createdApiKey.token)
expect(authenticated).toBeFalsy()
})
})
describe("retrieving API keys", () => { describe("retrieving API keys", () => {
it("should successfully return all existing api keys", async function () { it("should successfully return all existing api keys", async function () {
await service.create([ await service.create([
@@ -13,7 +13,12 @@ import {
} from "@medusajs/types" } from "@medusajs/types"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import { ApiKey } from "@models" import { ApiKey } from "@models"
import { CreateApiKeyDTO, TokenDTO } from "@types" import {
CreateApiKeyDTO,
RevokeApiKeyInput,
TokenDTO,
UpdateApiKeyInput,
} from "@types"
import { import {
ApiKeyType, ApiKeyType,
InjectManager, InjectManager,
@@ -23,6 +28,7 @@ import {
ModulesSdkUtils, ModulesSdkUtils,
isObject, isObject,
isString, isString,
promiseAll,
} from "@medusajs/utils" } from "@medusajs/utils"
const scrypt = util.promisify(crypto.scrypt) const scrypt = util.promisify(crypto.scrypt)
@@ -132,61 +138,111 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
return [createdApiKeys, generatedTokens] return [createdApiKeys, generatedTokens]
} }
async update( async upsert(
selector: FilterableApiKeyProps, data: ApiKeyTypes.UpsertApiKeyDTO[],
data: Omit<ApiKeyTypes.UpdateApiKeyDTO, "id">,
sharedContext?: Context sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO[]> ): Promise<ApiKeyTypes.ApiKeyDTO[]>
async upsert(
data: ApiKeyTypes.UpsertApiKeyDTO,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO>
@InjectManager("baseRepository_")
async upsert(
data: ApiKeyTypes.UpsertApiKeyDTO | ApiKeyTypes.UpsertApiKeyDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<ApiKeyTypes.ApiKeyDTO | ApiKeyTypes.ApiKeyDTO[]> {
const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter(
(apiKey): apiKey is UpdateApiKeyInput => !!apiKey.id
)
const forCreate = input.filter(
(apiKey): apiKey is ApiKeyTypes.CreateApiKeyDTO => !apiKey.id
)
const operations: Promise<ApiKeyTypes.ApiKeyDTO[]>[] = []
if (forCreate.length) {
const op = async () => {
const [createdApiKeys, generatedTokens] = await this.create_(
forCreate,
sharedContext
)
const serializedResponse = await this.baseRepository_.serialize<
ApiKeyTypes.ApiKeyDTO[]
>(createdApiKeys, {
populate: true,
})
return serializedResponse.map(
(key) =>
({
...key,
token:
generatedTokens.find((t) => t.hashedToken === key.token)
?.rawToken ?? key.token,
salt: undefined,
} as ApiKeyTypes.ApiKeyDTO)
)
}
operations.push(op())
}
if (forUpdate.length) {
const op = async () => {
const updateResp = await this.update_(forUpdate, sharedContext)
return await this.baseRepository_.serialize<ApiKeyTypes.ApiKeyDTO[]>(
updateResp
)
}
operations.push(op())
}
const result = (await promiseAll(operations)).flat()
return Array.isArray(data) ? result : result[0]
}
async update( async update(
id: string, id: string,
data: Omit<ApiKeyTypes.UpdateApiKeyDTO, "id">, data: ApiKeyTypes.UpdateApiKeyDTO,
sharedContext?: Context sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO> ): Promise<ApiKeyTypes.ApiKeyDTO>
async update( async update(
data: ApiKeyTypes.UpdateApiKeyDTO[] selector: FilterableApiKeyProps,
data: ApiKeyTypes.UpdateApiKeyDTO,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO[]> ): Promise<ApiKeyTypes.ApiKeyDTO[]>
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
async update( async update(
idOrSelectorOrData: idOrSelector: string | FilterableApiKeyProps,
| string data: ApiKeyTypes.UpdateApiKeyDTO,
| FilterableApiKeyProps
| ApiKeyTypes.UpdateApiKeyDTO[],
data?: Omit<ApiKeyTypes.UpdateApiKeyDTO, "id">,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<ApiKeyTypes.ApiKeyDTO[] | ApiKeyTypes.ApiKeyDTO> { ): Promise<ApiKeyTypes.ApiKeyDTO[] | ApiKeyTypes.ApiKeyDTO> {
const updatedApiKeys = await this.update_( let normalizedInput = await this.normalizeUpdateInput_<UpdateApiKeyInput>(
idOrSelectorOrData, idOrSelector,
data, data,
sharedContext sharedContext
) )
const updatedApiKeys = await this.update_(normalizedInput, sharedContext)
const serializedResponse = await this.baseRepository_.serialize< const serializedResponse = await this.baseRepository_.serialize<
ApiKeyTypes.ApiKeyDTO[] ApiKeyTypes.ApiKeyDTO[]
>(updatedApiKeys.map(omitToken), { >(updatedApiKeys.map(omitToken), {
populate: true, populate: true,
}) })
return isString(idOrSelectorOrData) return isString(idOrSelector) ? serializedResponse[0] : serializedResponse
? serializedResponse[0]
: serializedResponse
} }
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
protected async update_( protected async update_(
idOrSelectorOrData: normalizedInput: UpdateApiKeyInput[],
| string
| FilterableApiKeyProps
| ApiKeyTypes.UpdateApiKeyDTO[],
data?: Omit<ApiKeyTypes.UpdateApiKeyDTO, "id">,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> { ): Promise<TEntity[]> {
const normalizedInput =
await this.normalizeUpdateInput_<ApiKeyTypes.UpdateApiKeyDTO>(
idOrSelectorOrData,
data,
sharedContext
)
const updateRequest = normalizedInput.map((k) => ({ const updateRequest = normalizedInput.map((k) => ({
id: k.id, id: k.id,
title: k.title, title: k.title,
@@ -258,33 +314,28 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
] ]
} }
async revoke(
selector: FilterableApiKeyProps,
data: Omit<ApiKeyTypes.RevokeApiKeyDTO, "id">,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO[]>
async revoke( async revoke(
id: string, id: string,
data: Omit<ApiKeyTypes.RevokeApiKeyDTO, "id">, data: ApiKeyTypes.RevokeApiKeyDTO,
sharedContext?: Context sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO> ): Promise<ApiKeyTypes.ApiKeyDTO>
async revoke( async revoke(
data: ApiKeyTypes.RevokeApiKeyDTO[] selector: FilterableApiKeyProps,
data: ApiKeyTypes.RevokeApiKeyDTO,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO[]> ): Promise<ApiKeyTypes.ApiKeyDTO[]>
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
async revoke( async revoke(
idOrSelectorOrData: idOrSelector: string | FilterableApiKeyProps,
| string data: ApiKeyTypes.RevokeApiKeyDTO,
| FilterableApiKeyProps
| ApiKeyTypes.RevokeApiKeyDTO[],
data?: Omit<ApiKeyTypes.RevokeApiKeyDTO, "id">,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<ApiKeyTypes.ApiKeyDTO[] | ApiKeyTypes.ApiKeyDTO> { ): Promise<ApiKeyTypes.ApiKeyDTO[] | ApiKeyTypes.ApiKeyDTO> {
const revokedApiKeys = await this.revoke_( const normalizedInput = await this.normalizeUpdateInput_<RevokeApiKeyInput>(
idOrSelectorOrData, idOrSelector,
data, data,
sharedContext sharedContext
) )
const revokedApiKeys = await this.revoke_(normalizedInput, sharedContext)
const serializedResponse = await this.baseRepository_.serialize< const serializedResponse = await this.baseRepository_.serialize<
ApiKeyTypes.ApiKeyDTO[] ApiKeyTypes.ApiKeyDTO[]
@@ -292,34 +343,28 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
populate: true, populate: true,
}) })
return isString(idOrSelectorOrData) return isString(idOrSelector) ? serializedResponse[0] : serializedResponse
? serializedResponse[0]
: serializedResponse
} }
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
async revoke_( async revoke_(
idOrSelectorOrData: normalizedInput: RevokeApiKeyInput[],
| string
| FilterableApiKeyProps
| ApiKeyTypes.RevokeApiKeyDTO[],
data?: Omit<ApiKeyTypes.RevokeApiKeyDTO, "id">,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> { ): Promise<TEntity[]> {
const normalizedInput =
await this.normalizeUpdateInput_<ApiKeyTypes.RevokeApiKeyDTO>(
idOrSelectorOrData,
data,
sharedContext
)
await this.validateRevokeApiKeys_(normalizedInput) await this.validateRevokeApiKeys_(normalizedInput)
const updateRequest = normalizedInput.map((k) => ({ const updateRequest = normalizedInput.map((k) => {
id: k.id, const revokedAt = new Date()
revoked_at: new Date(), if (k.revoke_in && k.revoke_in > 0) {
revoked_by: k.revoked_by, revokedAt.setSeconds(revokedAt.getSeconds() + k.revoke_in)
})) }
return {
id: k.id,
revoked_at: revokedAt,
revoked_by: k.revoked_by,
}
})
const revokedApiKeys = await this.apiKeyService_.update( const revokedApiKeys = await this.apiKeyService_.update(
updateRequest, updateRequest,
@@ -329,13 +374,63 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
return revokedApiKeys return revokedApiKeys
} }
// TODO: Implement @InjectManager("baseRepository_")
@InjectTransactionManager("baseRepository_") async authenticate(
authenticate( token: string,
id: string,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<boolean> { ): Promise<ApiKeyTypes.ApiKeyDTO | false> {
return Promise.resolve(false) const result = await this.authenticate_(token, sharedContext)
if (!result) {
return false
}
const serialized =
await this.baseRepository_.serialize<ApiKeyTypes.ApiKeyDTO>(result, {
populate: true,
})
return serialized
}
@InjectTransactionManager("baseRepository_")
protected async authenticate_(
token: string,
@MedusaContext() sharedContext: Context = {}
): Promise<ApiKey | false> {
// Since we only allow up to 2 active tokens, getitng the list and checking each token isn't an issue.
// We can always filter on the redacted key if we add support for an arbitrary number of tokens.
const secretKeys = await this.apiKeyService_.list(
{
type: ApiKeyType.SECRET,
// If the revoke date is set in the future, it means the key is still valid.
$or: [
{ revoked_at: { $eq: null } },
{ revoked_at: { $gt: new Date() } },
],
},
{ take: null },
sharedContext
)
const matches = await promiseAll(
secretKeys.map(async (dbKey) => {
const hashedInput = await ApiKeyModuleService.calculateHash(
token,
dbKey.salt
)
if (hashedInput === dbKey.token) {
return dbKey
}
return undefined
})
)
const matchedKeys = matches.filter((match) => !!match)
if (!matchedKeys.length) {
return false
}
return matchedKeys[0]!
} }
protected async validateCreateApiKeys_( protected async validateCreateApiKeys_(
@@ -359,9 +454,12 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
const dbSecretKeys = await this.apiKeyService_.list( const dbSecretKeys = await this.apiKeyService_.list(
{ {
type: ApiKeyType.SECRET, type: ApiKeyType.SECRET,
revoked_at: null, $or: [
{ revoked_at: { $eq: null } },
{ revoked_at: { $gt: new Date() } },
],
}, },
{}, { take: null },
sharedContext sharedContext
) )
@@ -374,22 +472,18 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
} }
protected async normalizeUpdateInput_<T>( protected async normalizeUpdateInput_<T>(
idOrSelectorOrData: string | FilterableApiKeyProps | T[], idOrSelector: string | FilterableApiKeyProps,
data?: Omit<T, "id">, data: Omit<T, "id">,
sharedContext: Context = {} sharedContext: Context = {}
): Promise<T[]> { ): Promise<T[]> {
let normalizedInput: T[] = [] let normalizedInput: T[] = []
if (isString(idOrSelectorOrData)) { if (isString(idOrSelector)) {
normalizedInput = [{ id: idOrSelectorOrData, ...data } as T] normalizedInput = [{ id: idOrSelector, ...data } as T]
} }
if (Array.isArray(idOrSelectorOrData)) { if (isObject(idOrSelector)) {
normalizedInput = idOrSelectorOrData
}
if (isObject(idOrSelectorOrData)) {
const apiKeys = await this.apiKeyService_.list( const apiKeys = await this.apiKeyService_.list(
idOrSelectorOrData, idOrSelector,
{}, {},
sharedContext sharedContext
) )
@@ -407,7 +501,7 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
} }
protected async validateRevokeApiKeys_( protected async validateRevokeApiKeys_(
data: ApiKeyTypes.RevokeApiKeyDTO[], data: RevokeApiKeyInput[],
sharedContext: Context = {} sharedContext: Context = {}
): Promise<void> { ): Promise<void> {
if (!data.length) { if (!data.length) {
@@ -461,7 +555,7 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
protected static async generateSecretKey(): Promise<TokenDTO> { protected static async generateSecretKey(): Promise<TokenDTO> {
const token = "sk_" + crypto.randomBytes(32).toString("hex") const token = "sk_" + crypto.randomBytes(32).toString("hex")
const salt = crypto.randomBytes(16).toString("hex") const salt = crypto.randomBytes(16).toString("hex")
const hashed = ((await scrypt(token, salt, 64)) as Buffer).toString("hex") const hashed = await this.calculateHash(token, salt)
return { return {
rawToken: token, rawToken: token,
@@ -470,6 +564,13 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
redacted: redactKey(token), redacted: redactKey(token),
} }
} }
protected static async calculateHash(
token: string,
salt: string
): Promise<string> {
return ((await scrypt(token, salt, 64)) as Buffer).toString("hex")
}
} }
// We are mutating the object here as what microORM relies on non-enumerable fields for serialization, among other things. // We are mutating the object here as what microORM relies on non-enumerable fields for serialization, among other things.
+4 -1
View File
@@ -1,4 +1,4 @@
import { ApiKeyType } from "@medusajs/types" import { ApiKeyType, RevokeApiKeyDTO, UpdateApiKeyDTO } from "@medusajs/types"
import { IEventBusModuleService, Logger } from "@medusajs/types" import { IEventBusModuleService, Logger } from "@medusajs/types"
export type InitializeModuleInjectableDependencies = { export type InitializeModuleInjectableDependencies = {
@@ -21,3 +21,6 @@ export type TokenDTO = {
salt: string salt: string
redacted: string redacted: string
} }
export type UpdateApiKeyInput = UpdateApiKeyDTO & { id: string }
export type RevokeApiKeyInput = RevokeApiKeyDTO & { id: string }
@@ -4,12 +4,11 @@ import {
IApiKeyModuleService, IApiKeyModuleService,
RevokeApiKeyDTO, RevokeApiKeyDTO,
} from "@medusajs/types" } from "@medusajs/types"
import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk" import { StepResponse, createStep } from "@medusajs/workflows-sdk"
type RevokeApiKeysStepInput = { type RevokeApiKeysStepInput = {
selector: FilterableApiKeyProps selector: FilterableApiKeyProps
revoke: Omit<RevokeApiKeyDTO, "id"> revoke: RevokeApiKeyDTO
} }
export const revokeApiKeysStepId = "revoke-api-keys" export const revokeApiKeysStepId = "revoke-api-keys"
@@ -9,7 +9,7 @@ import { StepResponse, createStep } from "@medusajs/workflows-sdk"
type UpdateApiKeysStepInput = { type UpdateApiKeysStepInput = {
selector: FilterableApiKeyProps selector: FilterableApiKeyProps
update: Omit<UpdateApiKeyDTO, "id"> update: UpdateApiKeyDTO
} }
export const updateApiKeysStepId = "update-api-keys" export const updateApiKeysStepId = "update-api-keys"
@@ -41,7 +41,7 @@ export const updateApiKeysStep = createStep(
ModuleRegistrationName.API_KEY ModuleRegistrationName.API_KEY
) )
await service.update( await service.upsert(
prevData.map((r) => ({ prevData.map((r) => ({
id: r.id, id: r.id,
title: r.title, title: r.title,
@@ -8,7 +8,7 @@ import { revokeApiKeysStep } from "../steps"
type RevokeApiKeysStepInput = { type RevokeApiKeysStepInput = {
selector: FilterableApiKeyProps selector: FilterableApiKeyProps
revoke: Omit<RevokeApiKeyDTO, "id"> revoke: RevokeApiKeyDTO
} }
type WorkflowInput = RevokeApiKeysStepInput type WorkflowInput = RevokeApiKeysStepInput
@@ -8,7 +8,7 @@ import { updateApiKeysStep } from "../steps"
type UpdateApiKeysStepInput = { type UpdateApiKeysStepInput = {
selector: FilterableApiKeyProps selector: FilterableApiKeyProps
update: Omit<UpdateApiKeyDTO, "id"> update: UpdateApiKeyDTO
} }
type WorkflowInput = UpdateApiKeysStepInput type WorkflowInput = UpdateApiKeysStepInput
@@ -16,6 +16,7 @@ export const POST = async (
input: { input: {
selector: { id: req.params.id }, selector: { id: req.params.id },
revoke: { revoke: {
...(req.validatedBody as Omit<RevokeApiKeyDTO, "revoked_by">),
revoked_by: req.auth.actor_id, revoked_by: req.auth.actor_id,
} as RevokeApiKeyDTO, } as RevokeApiKeyDTO,
}, },
@@ -37,7 +37,7 @@ export const POST = async (
const { result, errors } = await updateApiKeysWorkflow(req.scope).run({ const { result, errors } = await updateApiKeysWorkflow(req.scope).run({
input: { input: {
selector: { id: req.params.id }, selector: { id: req.params.id },
update: req.validatedBody, update: req.validatedBody as UpdateApiKeyDTO,
}, },
throwOnError: false, throwOnError: false,
}) })
@@ -3,6 +3,7 @@ import { Type } from "class-transformer"
import { import {
IsArray, IsArray,
IsEnum, IsEnum,
IsNumber,
IsOptional, IsOptional,
IsString, IsString,
ValidateNested, ValidateNested,
@@ -65,6 +66,10 @@ export class AdminPostApiKeysApiKeyReq {
title: string title: string
} }
export class AdminRevokeApiKeysApiKeyReq {} export class AdminRevokeApiKeysApiKeyReq {
@IsOptional()
@IsNumber()
revoke_in?: number
}
export class AdminDeleteApiKeysApiKeyReq {} export class AdminDeleteApiKeysApiKeyReq {}
@@ -7,12 +7,18 @@ export interface CreateApiKeyDTO {
// We could add revoked_at as a parameter (or expires_at that gets mapped to revoked_at internally) in order to support expiring tokens // 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 UpsertApiKeyDTO {
id?: string
title?: string
type?: ApiKeyType
created_by?: string
}
export interface UpdateApiKeyDTO { export interface UpdateApiKeyDTO {
id: string
title?: string title?: string
} }
export interface RevokeApiKeyDTO { export interface RevokeApiKeyDTO {
id: string
revoked_by: string revoked_by: string
revoke_in?: number // Seconds after which the token should be considered revoked
} }
+55 -27
View File
@@ -2,7 +2,12 @@ import { IModuleService } from "../modules-sdk"
import { ApiKeyDTO, FilterableApiKeyProps } from "./common" import { ApiKeyDTO, FilterableApiKeyProps } from "./common"
import { FindConfig } from "../common" import { FindConfig } from "../common"
import { Context } from "../shared-context" import { Context } from "../shared-context"
import { CreateApiKeyDTO, RevokeApiKeyDTO, UpdateApiKeyDTO } from "./mutations" import {
CreateApiKeyDTO,
RevokeApiKeyDTO,
UpdateApiKeyDTO,
UpsertApiKeyDTO,
} from "./mutations"
export interface IApiKeyModuleService extends IModuleService { export interface IApiKeyModuleService extends IModuleService {
/** /**
@@ -14,32 +19,57 @@ export interface IApiKeyModuleService extends IModuleService {
create(data: CreateApiKeyDTO, sharedContext?: Context): Promise<ApiKeyDTO> create(data: CreateApiKeyDTO, sharedContext?: Context): Promise<ApiKeyDTO>
/** /**
* Update an api key * This method updates existing API keys, or creates new ones if they don't exist.
* @param selector *
* @param data * @param {UpsertApiKeyDTO[]} data - The attributes to update or create for each API key.
* @param sharedContext * @returns {Promise<ApiKeyDTO[]>} The updated and created API keys.
*
* @example
* {example-code}
*/ */
update( upsert(data: UpsertApiKeyDTO[], sharedContext?: Context): Promise<ApiKeyDTO[]>
selector: FilterableApiKeyProps,
data: Omit<UpdateApiKeyDTO, "id">,
sharedContext?: Context
): Promise<ApiKeyDTO[]>
/** /**
* Update an api key * This method updates an existing API key, or creates a new one if it doesn't exist.
* @param id *
* @param data * @param {UpsertApiKeyDTO} data - The attributes to update or create for the API key.
* @param sharedContext * @returns {Promise<ApiKeyDTO>} The updated or created API key.
*
* @example
* {example-code}
*/
upsert(data: UpsertApiKeyDTO, sharedContext?: Context): Promise<ApiKeyDTO>
/**
* This method updates an existing API key.
*
* @param {string} id - The API key's ID.
* @param {UpdateApiKeyDTO} data - The details to update in the API key.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<ApiKeyDTO>} The updated API key.
*/ */
update( update(
id: string, id: string,
data: Omit<UpdateApiKeyDTO, "id">, data: UpdateApiKeyDTO,
sharedContext?: Context sharedContext?: Context
): Promise<ApiKeyDTO> ): Promise<ApiKeyDTO>
/** /**
* Update an api key * This method updates existing API keys.
* @param data *
* @param {FilterableApiKeyProps} selector - The filters to specify which API keys should be updated.
* @param {UpdateApiKeyDTO} data - The details to update in the API keys.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<ApiKeyDTO[]>} The updated API keys.
*
* @example
* {example-code}
*/ */
update(data: UpdateApiKeyDTO[]): Promise<ApiKeyDTO[]> update(
selector: FilterableApiKeyProps,
data: UpdateApiKeyDTO,
sharedContext?: Context
): Promise<ApiKeyDTO[]>
/** /**
* Delete an api key * Delete an api key
@@ -93,7 +123,7 @@ export interface IApiKeyModuleService extends IModuleService {
*/ */
revoke( revoke(
selector: FilterableApiKeyProps, selector: FilterableApiKeyProps,
data: Omit<RevokeApiKeyDTO, "id">, data: RevokeApiKeyDTO,
sharedContext?: Context sharedContext?: Context
): Promise<ApiKeyDTO[]> ): Promise<ApiKeyDTO[]>
/** /**
@@ -104,19 +134,17 @@ export interface IApiKeyModuleService extends IModuleService {
*/ */
revoke( revoke(
id: string, id: string,
data: Omit<RevokeApiKeyDTO, "id">, data: RevokeApiKeyDTO,
sharedContext?: Context sharedContext?: Context
): Promise<ApiKeyDTO> ): Promise<ApiKeyDTO>
/**
* Revokes an api key
* @param data
*/
revoke(data: RevokeApiKeyDTO[]): Promise<ApiKeyDTO[]>
/** /**
* Check the validity of an api key * Check the validity of an api key
* @param id * @param token
* @param sharedContext * @param sharedContext
*/ */
authenticate(id: string, sharedContext?: Context): Promise<boolean> authenticate(
token: string,
sharedContext?: Context
): Promise<ApiKeyDTO | false>
} }