chore: Updates based on PR feedback
This commit is contained in:
@@ -53,7 +53,7 @@ moduleIntegrationTestRunner({
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: "Test API Key",
|
title: "Test API Key",
|
||||||
type: ApiKeyType.PUBLISHABLE,
|
type: ApiKeyType.PUBLISHABLE,
|
||||||
salt: "",
|
salt: undefined,
|
||||||
created_by: "test",
|
created_by: "test",
|
||||||
last_used_at: null,
|
last_used_at: null,
|
||||||
revoked_by: null,
|
revoked_by: null,
|
||||||
@@ -75,7 +75,7 @@ moduleIntegrationTestRunner({
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: "Secret key",
|
title: "Secret key",
|
||||||
type: ApiKeyType.SECRET,
|
type: ApiKeyType.SECRET,
|
||||||
salt: "44de31ebcf085fa423fc584aa8540670",
|
salt: undefined,
|
||||||
created_by: "test",
|
created_by: "test",
|
||||||
last_used_at: null,
|
last_used_at: null,
|
||||||
revoked_by: null,
|
revoked_by: null,
|
||||||
@@ -136,6 +136,16 @@ moduleIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should do nothing if the revokal list is empty", async function () {
|
||||||
|
const firstApiKey = await service.create(createSecretKeyFixture)
|
||||||
|
let revokedKeys = await service.revoke([])
|
||||||
|
expect(revokedKeys).toHaveLength(0)
|
||||||
|
|
||||||
|
const apiKey = await service.retrieve(firstApiKey.id)
|
||||||
|
expect(apiKey.revoked_at).toBeFalsy()
|
||||||
|
expect(apiKey.revoked_by).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
it("should not allow revoking an already revoked API key", async function () {
|
it("should not allow revoking an already revoked API key", async function () {
|
||||||
const firstApiKey = await service.create(createSecretKeyFixture)
|
const firstApiKey = await service.create(createSecretKeyFixture)
|
||||||
await service.revoke({
|
await service.revoke({
|
||||||
@@ -180,7 +190,6 @@ moduleIntegrationTestRunner({
|
|||||||
|
|
||||||
// These should not be returned on an update
|
// These should not be returned on an update
|
||||||
createdApiKey.token = ""
|
createdApiKey.token = ""
|
||||||
createdApiKey.salt = ""
|
|
||||||
expect(createdApiKey).toEqual(updatedApiKey)
|
expect(createdApiKey).toEqual(updatedApiKey)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -113,6 +113,16 @@
|
|||||||
"name": "api_key",
|
"name": "api_key",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
"indexes": [
|
"indexes": [
|
||||||
|
{
|
||||||
|
"keyName": "IDX_api_key_token_unique",
|
||||||
|
"columnNames": [
|
||||||
|
"token"
|
||||||
|
],
|
||||||
|
"composite": false,
|
||||||
|
"primary": false,
|
||||||
|
"unique": false,
|
||||||
|
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_api_key_token_unique\" ON \"api_key\" (token)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"keyName": "IDX_api_key_type",
|
"keyName": "IDX_api_key_type",
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
|
|||||||
+4
-1
@@ -1,10 +1,13 @@
|
|||||||
import { Migration } from "@mikro-orm/migrations"
|
import { Migration } from "@mikro-orm/migrations"
|
||||||
|
|
||||||
export class InitialSetup20240220155605 extends Migration {
|
export class InitialSetup20240221144943 extends Migration {
|
||||||
async up(): Promise<void> {
|
async up(): Promise<void> {
|
||||||
this.addSql(
|
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"));'
|
'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 UNIQUE INDEX IF NOT EXISTS "IDX_api_key_token_unique" ON "api_key" (token);'
|
||||||
|
)
|
||||||
this.addSql(
|
this.addSql(
|
||||||
'CREATE INDEX IF NOT EXISTS "IDX_api_key_type" ON "api_key" (type);'
|
'CREATE INDEX IF NOT EXISTS "IDX_api_key_type" ON "api_key" (type);'
|
||||||
)
|
)
|
||||||
@@ -17,12 +17,19 @@ const TypeIndex = createPsqlIndexStatementHelper({
|
|||||||
columns: "type",
|
columns: "type",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const TokenIndex = createPsqlIndexStatementHelper({
|
||||||
|
tableName: "api_key",
|
||||||
|
columns: "token",
|
||||||
|
unique: true,
|
||||||
|
})
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export default class ApiKey {
|
export default class ApiKey {
|
||||||
@PrimaryKey({ columnType: "text" })
|
@PrimaryKey({ columnType: "text" })
|
||||||
id: string
|
id: string
|
||||||
|
|
||||||
@Property({ columnType: "text" })
|
@Property({ columnType: "text" })
|
||||||
|
@TokenIndex.MikroORMIndex()
|
||||||
token: string
|
token: string
|
||||||
|
|
||||||
@Property({ columnType: "text" })
|
@Property({ columnType: "text" })
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
|
|||||||
token:
|
token:
|
||||||
generatedTokens.find((t) => t.hashedToken === key.token)?.rawToken ??
|
generatedTokens.find((t) => t.hashedToken === key.token)?.rawToken ??
|
||||||
key.token,
|
key.token,
|
||||||
|
salt: undefined,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return Array.isArray(data) ? responseWithRawToken : responseWithRawToken[0]
|
return Array.isArray(data) ? responseWithRawToken : responseWithRawToken[0]
|
||||||
@@ -99,7 +100,7 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
|
|||||||
data: ApiKeyTypes.CreateApiKeyDTO[],
|
data: ApiKeyTypes.CreateApiKeyDTO[],
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<[TEntity[], TokenDTO[]]> {
|
): Promise<[TEntity[], TokenDTO[]]> {
|
||||||
await this.validateCreateApiKeys(data, sharedContext)
|
await this.validateCreateApiKeys_(data, sharedContext)
|
||||||
|
|
||||||
const normalizedInput: CreateApiKeyDTO[] = []
|
const normalizedInput: CreateApiKeyDTO[] = []
|
||||||
const generatedTokens: TokenDTO[] = []
|
const generatedTokens: TokenDTO[] = []
|
||||||
@@ -201,7 +202,7 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
|
|||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
|
|
||||||
return this.baseRepository_.serialize<ApiKeyTypes.ApiKeyDTO[]>(
|
return await this.baseRepository_.serialize<ApiKeyTypes.ApiKeyDTO[]>(
|
||||||
apiKeys.map(omitToken),
|
apiKeys.map(omitToken),
|
||||||
{
|
{
|
||||||
populate: true,
|
populate: true,
|
||||||
@@ -215,17 +216,15 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
|
|||||||
config?: FindConfig<ApiKeyTypes.ApiKeyDTO>,
|
config?: FindConfig<ApiKeyTypes.ApiKeyDTO>,
|
||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<[ApiKeyTypes.ApiKeyDTO[], number]> {
|
): Promise<[ApiKeyTypes.ApiKeyDTO[], number]> {
|
||||||
const result = await this.apiKeyService_.listAndCount(
|
const [apiKeys, count] = await this.apiKeyService_.listAndCount(
|
||||||
filters,
|
filters,
|
||||||
config,
|
config,
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
const withoutToken = result[0].map(omitToken)
|
|
||||||
const count = result[1]
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
await this.baseRepository_.serialize<ApiKeyTypes.ApiKeyDTO[]>(
|
await this.baseRepository_.serialize<ApiKeyTypes.ApiKeyDTO[]>(
|
||||||
withoutToken,
|
apiKeys.map(omitToken),
|
||||||
{
|
{
|
||||||
populate: true,
|
populate: true,
|
||||||
}
|
}
|
||||||
@@ -267,7 +266,7 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
|
|||||||
data: ApiKeyTypes.RevokeApiKeyDTO[],
|
data: ApiKeyTypes.RevokeApiKeyDTO[],
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<TEntity[]> {
|
): Promise<TEntity[]> {
|
||||||
await this.validateRevokeApiKeys(data)
|
await this.validateRevokeApiKeys_(data)
|
||||||
|
|
||||||
const updateRequest = data.map((k) => ({
|
const updateRequest = data.map((k) => ({
|
||||||
id: k.id,
|
id: k.id,
|
||||||
@@ -292,7 +291,7 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
|
|||||||
return Promise.resolve(false)
|
return Promise.resolve(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async validateCreateApiKeys(
|
protected async validateCreateApiKeys_(
|
||||||
data: ApiKeyTypes.CreateApiKeyDTO[],
|
data: ApiKeyTypes.CreateApiKeyDTO[],
|
||||||
sharedContext: Context = {}
|
sharedContext: Context = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -327,7 +326,7 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async validateRevokeApiKeys(
|
protected async validateRevokeApiKeys_(
|
||||||
data: ApiKeyTypes.RevokeApiKeyDTO[],
|
data: ApiKeyTypes.RevokeApiKeyDTO[],
|
||||||
sharedContext: Context = {}
|
sharedContext: Context = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -335,6 +334,13 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.some((k) => !k.id)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`You must provide an api key id field when revoking a key.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (data.some((k) => !k.revoked_by)) {
|
if (data.some((k) => !k.revoked_by)) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
@@ -387,9 +393,12 @@ export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
const omitToken = (key: ApiKey): ApiKey => {
|
const omitToken = (
|
||||||
|
// We have to make salt optional before deleting it (and we do want it required in the DB)
|
||||||
|
key: Omit<ApiKey, "salt"> & { salt?: string }
|
||||||
|
): Omit<ApiKey, "salt"> => {
|
||||||
key.token = key.type === ApiKeyType.SECRET ? "" : key.token
|
key.token = key.type === ApiKeyType.SECRET ? "" : key.token
|
||||||
key.salt = ""
|
delete key.salt
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user