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:
@@ -108,4 +108,29 @@ describe("API Keys - Admin", () => {
|
||||
expect(deleted.status).toEqual(200)
|
||||
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 () {
|
||||
const firstApiKey = await service.create(createSecretKeyFixture)
|
||||
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", () => {
|
||||
it("should successfully return all existing api keys", async function () {
|
||||
await service.create([
|
||||
|
||||
@@ -13,7 +13,12 @@ import {
|
||||
} from "@medusajs/types"
|
||||
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
|
||||
import { ApiKey } from "@models"
|
||||
import { CreateApiKeyDTO, TokenDTO } from "@types"
|
||||
import {
|
||||
CreateApiKeyDTO,
|
||||
RevokeApiKeyInput,
|
||||
TokenDTO,
|
||||
UpdateApiKeyInput,
|
||||
} from "@types"
|
||||
import {
|
||||
ApiKeyType,
|
||||
InjectManager,
|
||||
@@ -23,6 +28,7 @@ import {
|
||||
ModulesSdkUtils,
|
||||
isObject,
|
||||
isString,
|
||||
promiseAll,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
const scrypt = util.promisify(crypto.scrypt)
|
||||
@@ -132,61 +138,111 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
|
||||
return [createdApiKeys, generatedTokens]
|
||||
}
|
||||
|
||||
async update(
|
||||
selector: FilterableApiKeyProps,
|
||||
data: Omit<ApiKeyTypes.UpdateApiKeyDTO, "id">,
|
||||
async upsert(
|
||||
data: ApiKeyTypes.UpsertApiKeyDTO[],
|
||||
sharedContext?: Context
|
||||
): 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(
|
||||
id: string,
|
||||
data: Omit<ApiKeyTypes.UpdateApiKeyDTO, "id">,
|
||||
data: ApiKeyTypes.UpdateApiKeyDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<ApiKeyTypes.ApiKeyDTO>
|
||||
async update(
|
||||
data: ApiKeyTypes.UpdateApiKeyDTO[]
|
||||
selector: FilterableApiKeyProps,
|
||||
data: ApiKeyTypes.UpdateApiKeyDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<ApiKeyTypes.ApiKeyDTO[]>
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async update(
|
||||
idOrSelectorOrData:
|
||||
| string
|
||||
| FilterableApiKeyProps
|
||||
| ApiKeyTypes.UpdateApiKeyDTO[],
|
||||
data?: Omit<ApiKeyTypes.UpdateApiKeyDTO, "id">,
|
||||
idOrSelector: string | FilterableApiKeyProps,
|
||||
data: ApiKeyTypes.UpdateApiKeyDTO,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<ApiKeyTypes.ApiKeyDTO[] | ApiKeyTypes.ApiKeyDTO> {
|
||||
const updatedApiKeys = await this.update_(
|
||||
idOrSelectorOrData,
|
||||
let normalizedInput = await this.normalizeUpdateInput_<UpdateApiKeyInput>(
|
||||
idOrSelector,
|
||||
data,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const updatedApiKeys = await this.update_(normalizedInput, sharedContext)
|
||||
|
||||
const serializedResponse = await this.baseRepository_.serialize<
|
||||
ApiKeyTypes.ApiKeyDTO[]
|
||||
>(updatedApiKeys.map(omitToken), {
|
||||
populate: true,
|
||||
})
|
||||
|
||||
return isString(idOrSelectorOrData)
|
||||
? serializedResponse[0]
|
||||
: serializedResponse
|
||||
return isString(idOrSelector) ? serializedResponse[0] : serializedResponse
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
protected async update_(
|
||||
idOrSelectorOrData:
|
||||
| string
|
||||
| FilterableApiKeyProps
|
||||
| ApiKeyTypes.UpdateApiKeyDTO[],
|
||||
data?: Omit<ApiKeyTypes.UpdateApiKeyDTO, "id">,
|
||||
normalizedInput: UpdateApiKeyInput[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
const normalizedInput =
|
||||
await this.normalizeUpdateInput_<ApiKeyTypes.UpdateApiKeyDTO>(
|
||||
idOrSelectorOrData,
|
||||
data,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const updateRequest = normalizedInput.map((k) => ({
|
||||
id: k.id,
|
||||
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(
|
||||
id: string,
|
||||
data: Omit<ApiKeyTypes.RevokeApiKeyDTO, "id">,
|
||||
data: ApiKeyTypes.RevokeApiKeyDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<ApiKeyTypes.ApiKeyDTO>
|
||||
async revoke(
|
||||
data: ApiKeyTypes.RevokeApiKeyDTO[]
|
||||
selector: FilterableApiKeyProps,
|
||||
data: ApiKeyTypes.RevokeApiKeyDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<ApiKeyTypes.ApiKeyDTO[]>
|
||||
@InjectManager("baseRepository_")
|
||||
async revoke(
|
||||
idOrSelectorOrData:
|
||||
| string
|
||||
| FilterableApiKeyProps
|
||||
| ApiKeyTypes.RevokeApiKeyDTO[],
|
||||
data?: Omit<ApiKeyTypes.RevokeApiKeyDTO, "id">,
|
||||
idOrSelector: string | FilterableApiKeyProps,
|
||||
data: ApiKeyTypes.RevokeApiKeyDTO,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<ApiKeyTypes.ApiKeyDTO[] | ApiKeyTypes.ApiKeyDTO> {
|
||||
const revokedApiKeys = await this.revoke_(
|
||||
idOrSelectorOrData,
|
||||
const normalizedInput = await this.normalizeUpdateInput_<RevokeApiKeyInput>(
|
||||
idOrSelector,
|
||||
data,
|
||||
sharedContext
|
||||
)
|
||||
const revokedApiKeys = await this.revoke_(normalizedInput, sharedContext)
|
||||
|
||||
const serializedResponse = await this.baseRepository_.serialize<
|
||||
ApiKeyTypes.ApiKeyDTO[]
|
||||
@@ -292,34 +343,28 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
|
||||
populate: true,
|
||||
})
|
||||
|
||||
return isString(idOrSelectorOrData)
|
||||
? serializedResponse[0]
|
||||
: serializedResponse
|
||||
return isString(idOrSelector) ? serializedResponse[0] : serializedResponse
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async revoke_(
|
||||
idOrSelectorOrData:
|
||||
| string
|
||||
| FilterableApiKeyProps
|
||||
| ApiKeyTypes.RevokeApiKeyDTO[],
|
||||
data?: Omit<ApiKeyTypes.RevokeApiKeyDTO, "id">,
|
||||
normalizedInput: RevokeApiKeyInput[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
const normalizedInput =
|
||||
await this.normalizeUpdateInput_<ApiKeyTypes.RevokeApiKeyDTO>(
|
||||
idOrSelectorOrData,
|
||||
data,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
await this.validateRevokeApiKeys_(normalizedInput)
|
||||
|
||||
const updateRequest = normalizedInput.map((k) => ({
|
||||
id: k.id,
|
||||
revoked_at: new Date(),
|
||||
revoked_by: k.revoked_by,
|
||||
}))
|
||||
const updateRequest = normalizedInput.map((k) => {
|
||||
const revokedAt = new Date()
|
||||
if (k.revoke_in && k.revoke_in > 0) {
|
||||
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(
|
||||
updateRequest,
|
||||
@@ -329,13 +374,63 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
|
||||
return revokedApiKeys
|
||||
}
|
||||
|
||||
// TODO: Implement
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
authenticate(
|
||||
id: string,
|
||||
@InjectManager("baseRepository_")
|
||||
async authenticate(
|
||||
token: string,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<boolean> {
|
||||
return Promise.resolve(false)
|
||||
): Promise<ApiKeyTypes.ApiKeyDTO | 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_(
|
||||
@@ -359,9 +454,12 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
|
||||
const dbSecretKeys = await this.apiKeyService_.list(
|
||||
{
|
||||
type: ApiKeyType.SECRET,
|
||||
revoked_at: null,
|
||||
$or: [
|
||||
{ revoked_at: { $eq: null } },
|
||||
{ revoked_at: { $gt: new Date() } },
|
||||
],
|
||||
},
|
||||
{},
|
||||
{ take: null },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
@@ -374,22 +472,18 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
|
||||
}
|
||||
|
||||
protected async normalizeUpdateInput_<T>(
|
||||
idOrSelectorOrData: string | FilterableApiKeyProps | T[],
|
||||
data?: Omit<T, "id">,
|
||||
idOrSelector: string | FilterableApiKeyProps,
|
||||
data: Omit<T, "id">,
|
||||
sharedContext: Context = {}
|
||||
): Promise<T[]> {
|
||||
let normalizedInput: T[] = []
|
||||
if (isString(idOrSelectorOrData)) {
|
||||
normalizedInput = [{ id: idOrSelectorOrData, ...data } as T]
|
||||
if (isString(idOrSelector)) {
|
||||
normalizedInput = [{ id: idOrSelector, ...data } as T]
|
||||
}
|
||||
|
||||
if (Array.isArray(idOrSelectorOrData)) {
|
||||
normalizedInput = idOrSelectorOrData
|
||||
}
|
||||
|
||||
if (isObject(idOrSelectorOrData)) {
|
||||
if (isObject(idOrSelector)) {
|
||||
const apiKeys = await this.apiKeyService_.list(
|
||||
idOrSelectorOrData,
|
||||
idOrSelector,
|
||||
{},
|
||||
sharedContext
|
||||
)
|
||||
@@ -407,7 +501,7 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
|
||||
}
|
||||
|
||||
protected async validateRevokeApiKeys_(
|
||||
data: ApiKeyTypes.RevokeApiKeyDTO[],
|
||||
data: RevokeApiKeyInput[],
|
||||
sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
if (!data.length) {
|
||||
@@ -461,7 +555,7 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
|
||||
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")
|
||||
const hashed = await this.calculateHash(token, salt)
|
||||
|
||||
return {
|
||||
rawToken: token,
|
||||
@@ -470,6 +564,13 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
|
||||
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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiKeyType } from "@medusajs/types"
|
||||
import { ApiKeyType, RevokeApiKeyDTO, UpdateApiKeyDTO } from "@medusajs/types"
|
||||
import { IEventBusModuleService, Logger } from "@medusajs/types"
|
||||
|
||||
export type InitializeModuleInjectableDependencies = {
|
||||
@@ -21,3 +21,6 @@ export type TokenDTO = {
|
||||
salt: string
|
||||
redacted: string
|
||||
}
|
||||
|
||||
export type UpdateApiKeyInput = UpdateApiKeyDTO & { id: string }
|
||||
export type RevokeApiKeyInput = RevokeApiKeyDTO & { id: string }
|
||||
|
||||
@@ -4,12 +4,11 @@ import {
|
||||
IApiKeyModuleService,
|
||||
RevokeApiKeyDTO,
|
||||
} from "@medusajs/types"
|
||||
import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
type RevokeApiKeysStepInput = {
|
||||
selector: FilterableApiKeyProps
|
||||
revoke: Omit<RevokeApiKeyDTO, "id">
|
||||
revoke: RevokeApiKeyDTO
|
||||
}
|
||||
|
||||
export const revokeApiKeysStepId = "revoke-api-keys"
|
||||
|
||||
@@ -9,7 +9,7 @@ import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
type UpdateApiKeysStepInput = {
|
||||
selector: FilterableApiKeyProps
|
||||
update: Omit<UpdateApiKeyDTO, "id">
|
||||
update: UpdateApiKeyDTO
|
||||
}
|
||||
|
||||
export const updateApiKeysStepId = "update-api-keys"
|
||||
@@ -41,7 +41,7 @@ export const updateApiKeysStep = createStep(
|
||||
ModuleRegistrationName.API_KEY
|
||||
)
|
||||
|
||||
await service.update(
|
||||
await service.upsert(
|
||||
prevData.map((r) => ({
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { revokeApiKeysStep } from "../steps"
|
||||
|
||||
type RevokeApiKeysStepInput = {
|
||||
selector: FilterableApiKeyProps
|
||||
revoke: Omit<RevokeApiKeyDTO, "id">
|
||||
revoke: RevokeApiKeyDTO
|
||||
}
|
||||
|
||||
type WorkflowInput = RevokeApiKeysStepInput
|
||||
|
||||
@@ -8,7 +8,7 @@ import { updateApiKeysStep } from "../steps"
|
||||
|
||||
type UpdateApiKeysStepInput = {
|
||||
selector: FilterableApiKeyProps
|
||||
update: Omit<UpdateApiKeyDTO, "id">
|
||||
update: UpdateApiKeyDTO
|
||||
}
|
||||
|
||||
type WorkflowInput = UpdateApiKeysStepInput
|
||||
|
||||
@@ -16,6 +16,7 @@ export const POST = async (
|
||||
input: {
|
||||
selector: { id: req.params.id },
|
||||
revoke: {
|
||||
...(req.validatedBody as Omit<RevokeApiKeyDTO, "revoked_by">),
|
||||
revoked_by: req.auth.actor_id,
|
||||
} as RevokeApiKeyDTO,
|
||||
},
|
||||
|
||||
@@ -37,7 +37,7 @@ export const POST = async (
|
||||
const { result, errors } = await updateApiKeysWorkflow(req.scope).run({
|
||||
input: {
|
||||
selector: { id: req.params.id },
|
||||
update: req.validatedBody,
|
||||
update: req.validatedBody as UpdateApiKeyDTO,
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Type } from "class-transformer"
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
@@ -65,6 +66,10 @@ export class AdminPostApiKeysApiKeyReq {
|
||||
title: string
|
||||
}
|
||||
|
||||
export class AdminRevokeApiKeysApiKeyReq {}
|
||||
export class AdminRevokeApiKeysApiKeyReq {
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
revoke_in?: number
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export interface UpsertApiKeyDTO {
|
||||
id?: string
|
||||
title?: string
|
||||
type?: ApiKeyType
|
||||
created_by?: string
|
||||
}
|
||||
|
||||
export interface UpdateApiKeyDTO {
|
||||
id: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export interface RevokeApiKeyDTO {
|
||||
id: string
|
||||
revoked_by: string
|
||||
revoke_in?: number // Seconds after which the token should be considered revoked
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ import { IModuleService } from "../modules-sdk"
|
||||
import { ApiKeyDTO, FilterableApiKeyProps } from "./common"
|
||||
import { FindConfig } from "../common"
|
||||
import { Context } from "../shared-context"
|
||||
import { CreateApiKeyDTO, RevokeApiKeyDTO, UpdateApiKeyDTO } from "./mutations"
|
||||
import {
|
||||
CreateApiKeyDTO,
|
||||
RevokeApiKeyDTO,
|
||||
UpdateApiKeyDTO,
|
||||
UpsertApiKeyDTO,
|
||||
} from "./mutations"
|
||||
|
||||
export interface IApiKeyModuleService extends IModuleService {
|
||||
/**
|
||||
@@ -14,32 +19,57 @@ export interface IApiKeyModuleService extends IModuleService {
|
||||
create(data: CreateApiKeyDTO, sharedContext?: Context): Promise<ApiKeyDTO>
|
||||
|
||||
/**
|
||||
* Update an api key
|
||||
* @param selector
|
||||
* @param data
|
||||
* @param sharedContext
|
||||
* This method updates existing API keys, or creates new ones if they don't exist.
|
||||
*
|
||||
* @param {UpsertApiKeyDTO[]} data - The attributes to update or create for each API key.
|
||||
* @returns {Promise<ApiKeyDTO[]>} The updated and created API keys.
|
||||
*
|
||||
* @example
|
||||
* {example-code}
|
||||
*/
|
||||
update(
|
||||
selector: FilterableApiKeyProps,
|
||||
data: Omit<UpdateApiKeyDTO, "id">,
|
||||
sharedContext?: Context
|
||||
): Promise<ApiKeyDTO[]>
|
||||
upsert(data: UpsertApiKeyDTO[], sharedContext?: Context): Promise<ApiKeyDTO[]>
|
||||
|
||||
/**
|
||||
* Update an api key
|
||||
* @param id
|
||||
* @param data
|
||||
* @param sharedContext
|
||||
* This method updates an existing API key, or creates a new one if it doesn't exist.
|
||||
*
|
||||
* @param {UpsertApiKeyDTO} data - The attributes to update or create for the API key.
|
||||
* @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(
|
||||
id: string,
|
||||
data: Omit<UpdateApiKeyDTO, "id">,
|
||||
data: UpdateApiKeyDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<ApiKeyDTO>
|
||||
|
||||
/**
|
||||
* Update an api key
|
||||
* @param data
|
||||
* This method updates existing API keys.
|
||||
*
|
||||
* @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
|
||||
@@ -93,7 +123,7 @@ export interface IApiKeyModuleService extends IModuleService {
|
||||
*/
|
||||
revoke(
|
||||
selector: FilterableApiKeyProps,
|
||||
data: Omit<RevokeApiKeyDTO, "id">,
|
||||
data: RevokeApiKeyDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<ApiKeyDTO[]>
|
||||
/**
|
||||
@@ -104,19 +134,17 @@ export interface IApiKeyModuleService extends IModuleService {
|
||||
*/
|
||||
revoke(
|
||||
id: string,
|
||||
data: Omit<RevokeApiKeyDTO, "id">,
|
||||
data: RevokeApiKeyDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<ApiKeyDTO>
|
||||
/**
|
||||
* Revokes an api key
|
||||
* @param data
|
||||
*/
|
||||
revoke(data: RevokeApiKeyDTO[]): Promise<ApiKeyDTO[]>
|
||||
|
||||
/**
|
||||
* Check the validity of an api key
|
||||
* @param id
|
||||
* @param token
|
||||
* @param sharedContext
|
||||
*/
|
||||
authenticate(id: string, sharedContext?: Context): Promise<boolean>
|
||||
authenticate(
|
||||
token: string,
|
||||
sharedContext?: Context
|
||||
): Promise<ApiKeyDTO | false>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user