feat: Add support for multiple active keys at a time (#13754)
This commit is contained in:
5
.changeset/tender-points-judge.md
Normal file
5
.changeset/tender-points-judge.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/api-key": patch
|
||||
---
|
||||
|
||||
Allow creating multiple active API keys at a time
|
||||
@@ -115,43 +115,18 @@ moduleIntegrationTestRunner<IApiKeyModuleService>({
|
||||
)
|
||||
})
|
||||
|
||||
it("should only allow creating one active token", async function () {
|
||||
await expect(
|
||||
service.createApiKeys([
|
||||
createSecretKeyFixture,
|
||||
createSecretKeyFixture,
|
||||
])
|
||||
).rejects.toThrow(
|
||||
"You can only create one secret key at a time. You tried to create 2 secret keys."
|
||||
)
|
||||
it("should allow creating multiple active token", async function () {
|
||||
const apiKeys = await service.createApiKeys([
|
||||
createSecretKeyFixture,
|
||||
createSecretKeyFixture,
|
||||
])
|
||||
|
||||
await service.createApiKeys(createSecretKeyFixture)
|
||||
const err = await service
|
||||
.createApiKeys(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."
|
||||
)
|
||||
})
|
||||
apiKeys.push(await service.createApiKeys(createSecretKeyFixture))
|
||||
|
||||
it("should allow for at most two tokens, where one is revoked", async function () {
|
||||
const firstApiKey = await service.createApiKeys(
|
||||
createSecretKeyFixture
|
||||
)
|
||||
await service.revoke(
|
||||
{ id: firstApiKey.id },
|
||||
{
|
||||
revoked_by: "test",
|
||||
}
|
||||
)
|
||||
|
||||
await service.createApiKeys(createSecretKeyFixture)
|
||||
const err = await service
|
||||
.createApiKeys(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."
|
||||
)
|
||||
expect(apiKeys).toHaveLength(3)
|
||||
expect(apiKeys[0].revoked_at).toBeFalsy()
|
||||
expect(apiKeys[1].revoked_at).toBeFalsy()
|
||||
expect(apiKeys[2].revoked_at).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -142,6 +142,7 @@
|
||||
"keyName": "IDX_api_key_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_api_key_deleted_at\" ON \"api_key\" (deleted_at) WHERE deleted_at IS NULL"
|
||||
@@ -150,14 +151,34 @@
|
||||
"keyName": "IDX_api_key_token_unique",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_api_key_token_unique\" ON \"api_key\" (token) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_api_key_revoked_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_api_key_revoked_at\" ON \"api_key\" (revoked_at) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_api_key_redacted",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_api_key_redacted\" ON \"api_key\" (redacted) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_api_key_type",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_api_key_type\" ON \"api_key\" (type) WHERE deleted_at IS NULL"
|
||||
@@ -168,12 +189,15 @@
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {}
|
||||
"foreignKeys": {},
|
||||
"nativeEnums": {}
|
||||
}
|
||||
]
|
||||
],
|
||||
"nativeEnums": {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20251015123842 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_api_key_revoked_at" ON "api_key" (revoked_at) WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_api_key_redacted" ON "api_key" (redacted) WHERE deleted_at IS NULL;`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`drop index if exists "IDX_api_key_revoked_at";`);
|
||||
this.addSql(`drop index if exists "IDX_api_key_redacted";`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,6 +18,12 @@ const ApiKey = model
|
||||
on: ["token"],
|
||||
unique: true,
|
||||
},
|
||||
{
|
||||
on: ["revoked_at"],
|
||||
},
|
||||
{
|
||||
on: ["redacted"],
|
||||
},
|
||||
{
|
||||
on: ["type"],
|
||||
},
|
||||
|
||||
@@ -155,8 +155,6 @@ export class ApiKeyModuleService
|
||||
data: ApiKeyTypes.CreateApiKeyDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[InferEntityType<typeof ApiKey>[], TokenDTO[]]> {
|
||||
await this.validateCreateApiKeys_(data, sharedContext)
|
||||
|
||||
const normalizedInput: CreateApiKeyDTO[] = []
|
||||
const generatedTokens: TokenDTO[] = []
|
||||
for (const key of data) {
|
||||
@@ -461,18 +459,21 @@ export class ApiKeyModuleService
|
||||
token: string,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<InferEntityType<typeof 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,
|
||||
// There could be many unrevoked keys at the same time, so we narrow the list down by the redacted key.
|
||||
// Note that the redacted key doesn't guarantee uniqueness and is not an authentication check, but just an optimization.
|
||||
redacted: redactKey(token),
|
||||
// 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: undefined,
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
|
||||
@@ -494,49 +495,7 @@ export class ApiKeyModuleService
|
||||
if (!matchedKeys.length) {
|
||||
return false
|
||||
}
|
||||
return matchedKeys[0]!
|
||||
}
|
||||
|
||||
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) {
|
||||
return
|
||||
}
|
||||
|
||||
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,
|
||||
$or: [
|
||||
{ revoked_at: { $eq: null } },
|
||||
{ revoked_at: { $gt: new Date() } },
|
||||
],
|
||||
},
|
||||
{},
|
||||
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.`
|
||||
)
|
||||
}
|
||||
return matchedKeys[0]
|
||||
}
|
||||
|
||||
protected async normalizeUpdateInput_<T>(
|
||||
@@ -594,7 +553,7 @@ export class ApiKeyModuleService
|
||||
{
|
||||
id: data.map((k) => k.id),
|
||||
type: ApiKeyType.SECRET,
|
||||
revoked_at: { $ne: null },
|
||||
revoked_at: { $lt: new Date() },
|
||||
},
|
||||
{},
|
||||
sharedContext
|
||||
|
||||
Reference in New Issue
Block a user