feat: Add support for multiple active keys at a time (#13754)
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/api-key": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Allow creating multiple active API keys at a time
|
||||||
+10
-35
@@ -115,43 +115,18 @@ moduleIntegrationTestRunner<IApiKeyModuleService>({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should only allow creating one active token", async function () {
|
it("should allow creating multiple active token", async function () {
|
||||||
await expect(
|
const apiKeys = await service.createApiKeys([
|
||||||
service.createApiKeys([
|
createSecretKeyFixture,
|
||||||
createSecretKeyFixture,
|
createSecretKeyFixture,
|
||||||
createSecretKeyFixture,
|
])
|
||||||
])
|
|
||||||
).rejects.toThrow(
|
|
||||||
"You can only create one secret key at a time. You tried to create 2 secret keys."
|
|
||||||
)
|
|
||||||
|
|
||||||
await service.createApiKeys(createSecretKeyFixture)
|
apiKeys.push(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."
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should allow for at most two tokens, where one is revoked", async function () {
|
expect(apiKeys).toHaveLength(3)
|
||||||
const firstApiKey = await service.createApiKeys(
|
expect(apiKeys[0].revoked_at).toBeFalsy()
|
||||||
createSecretKeyFixture
|
expect(apiKeys[1].revoked_at).toBeFalsy()
|
||||||
)
|
expect(apiKeys[2].revoked_at).toBeFalsy()
|
||||||
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."
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -142,6 +142,7 @@
|
|||||||
"keyName": "IDX_api_key_deleted_at",
|
"keyName": "IDX_api_key_deleted_at",
|
||||||
"columnNames": [],
|
"columnNames": [],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
|
"constraint": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": false,
|
"unique": false,
|
||||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_api_key_deleted_at\" ON \"api_key\" (deleted_at) WHERE deleted_at IS NULL"
|
"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",
|
"keyName": "IDX_api_key_token_unique",
|
||||||
"columnNames": [],
|
"columnNames": [],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
|
"constraint": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": false,
|
"unique": false,
|
||||||
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_api_key_token_unique\" ON \"api_key\" (token) WHERE deleted_at IS NULL"
|
"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",
|
"keyName": "IDX_api_key_type",
|
||||||
"columnNames": [],
|
"columnNames": [],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
|
"constraint": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": false,
|
"unique": false,
|
||||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_api_key_type\" ON \"api_key\" (type) WHERE deleted_at IS NULL"
|
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_api_key_type\" ON \"api_key\" (type) WHERE deleted_at IS NULL"
|
||||||
@@ -168,12 +189,15 @@
|
|||||||
"id"
|
"id"
|
||||||
],
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
|
"constraint": true,
|
||||||
"primary": true,
|
"primary": true,
|
||||||
"unique": true
|
"unique": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"checks": [],
|
"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"],
|
on: ["token"],
|
||||||
unique: true,
|
unique: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
on: ["revoked_at"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
on: ["redacted"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
on: ["type"],
|
on: ["type"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -155,8 +155,6 @@ export class ApiKeyModuleService
|
|||||||
data: ApiKeyTypes.CreateApiKeyDTO[],
|
data: ApiKeyTypes.CreateApiKeyDTO[],
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<[InferEntityType<typeof ApiKey>[], TokenDTO[]]> {
|
): Promise<[InferEntityType<typeof ApiKey>[], TokenDTO[]]> {
|
||||||
await this.validateCreateApiKeys_(data, sharedContext)
|
|
||||||
|
|
||||||
const normalizedInput: CreateApiKeyDTO[] = []
|
const normalizedInput: CreateApiKeyDTO[] = []
|
||||||
const generatedTokens: TokenDTO[] = []
|
const generatedTokens: TokenDTO[] = []
|
||||||
for (const key of data) {
|
for (const key of data) {
|
||||||
@@ -461,18 +459,21 @@ export class ApiKeyModuleService
|
|||||||
token: string,
|
token: string,
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<InferEntityType<typeof ApiKey> | false> {
|
): 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(
|
const secretKeys = await this.apiKeyService_.list(
|
||||||
{
|
{
|
||||||
type: ApiKeyType.SECRET,
|
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.
|
// If the revoke date is set in the future, it means the key is still valid.
|
||||||
$or: [
|
$or: [
|
||||||
{ revoked_at: { $eq: null } },
|
{ revoked_at: { $eq: null } },
|
||||||
{ revoked_at: { $gt: new Date() } },
|
{ revoked_at: { $gt: new Date() } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{},
|
{
|
||||||
|
take: undefined,
|
||||||
|
},
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -494,49 +495,7 @@ export class ApiKeyModuleService
|
|||||||
if (!matchedKeys.length) {
|
if (!matchedKeys.length) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return matchedKeys[0]!
|
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.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async normalizeUpdateInput_<T>(
|
protected async normalizeUpdateInput_<T>(
|
||||||
@@ -594,7 +553,7 @@ export class ApiKeyModuleService
|
|||||||
{
|
{
|
||||||
id: data.map((k) => k.id),
|
id: data.map((k) => k.id),
|
||||||
type: ApiKeyType.SECRET,
|
type: ApiKeyType.SECRET,
|
||||||
revoked_at: { $ne: null },
|
revoked_at: { $lt: new Date() },
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
sharedContext
|
sharedContext
|
||||||
|
|||||||
Reference in New Issue
Block a user