@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/api-key": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
refactor: migrate api key module to DML
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"namespaces": ["public"],
|
"namespaces": [
|
||||||
|
"public"
|
||||||
|
],
|
||||||
"name": "public",
|
"name": "public",
|
||||||
"tables": [
|
"tables": [
|
||||||
{
|
{
|
||||||
@@ -56,7 +58,11 @@
|
|||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"mappedType": "text"
|
"enumItems": [
|
||||||
|
"publishable",
|
||||||
|
"secret"
|
||||||
|
],
|
||||||
|
"mappedType": "enum"
|
||||||
},
|
},
|
||||||
"last_used_at": {
|
"last_used_at": {
|
||||||
"name": "last_used_at",
|
"name": "last_used_at",
|
||||||
@@ -77,6 +83,25 @@
|
|||||||
"nullable": false,
|
"nullable": false,
|
||||||
"mappedType": "text"
|
"mappedType": "text"
|
||||||
},
|
},
|
||||||
|
"revoked_by": {
|
||||||
|
"name": "revoked_by",
|
||||||
|
"type": "text",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": true,
|
||||||
|
"mappedType": "text"
|
||||||
|
},
|
||||||
|
"revoked_at": {
|
||||||
|
"name": "revoked_at",
|
||||||
|
"type": "timestamptz",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": true,
|
||||||
|
"length": 6,
|
||||||
|
"mappedType": "datetime"
|
||||||
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
"name": "created_at",
|
"name": "created_at",
|
||||||
"type": "timestamptz",
|
"type": "timestamptz",
|
||||||
@@ -99,17 +124,8 @@
|
|||||||
"default": "now()",
|
"default": "now()",
|
||||||
"mappedType": "datetime"
|
"mappedType": "datetime"
|
||||||
},
|
},
|
||||||
"revoked_by": {
|
"deleted_at": {
|
||||||
"name": "revoked_by",
|
"name": "deleted_at",
|
||||||
"type": "text",
|
|
||||||
"unsigned": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"primary": false,
|
|
||||||
"nullable": true,
|
|
||||||
"mappedType": "text"
|
|
||||||
},
|
|
||||||
"revoked_at": {
|
|
||||||
"name": "revoked_at",
|
|
||||||
"type": "timestamptz",
|
"type": "timestamptz",
|
||||||
"unsigned": false,
|
"unsigned": false,
|
||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
@@ -123,24 +139,34 @@
|
|||||||
"schema": "public",
|
"schema": "public",
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
{
|
||||||
"keyName": "IDX_api_key_token_unique",
|
"keyName": "IDX_api_key_deleted_at",
|
||||||
"columnNames": ["token"],
|
"columnNames": [],
|
||||||
"composite": false,
|
"composite": 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)"
|
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_api_key_deleted_at\" ON \"api_key\" (deleted_at) WHERE deleted_at IS NULL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"keyName": "IDX_api_key_token_unique",
|
||||||
|
"columnNames": [],
|
||||||
|
"composite": 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_type",
|
"keyName": "IDX_api_key_type",
|
||||||
"columnNames": ["type"],
|
"columnNames": [],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": false,
|
"unique": false,
|
||||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_api_key_type\" ON \"api_key\" (type)"
|
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_api_key_type\" ON \"api_key\" (type) WHERE deleted_at IS NULL"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"keyName": "api_key_pkey",
|
"keyName": "api_key_pkey",
|
||||||
"columnNames": ["id"],
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"primary": true,
|
"primary": true,
|
||||||
"unique": true
|
"unique": true
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { Migration } from "@mikro-orm/migrations"
|
||||||
|
|
||||||
|
export class Migration20241205122700 extends Migration {
|
||||||
|
async up(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
'alter table if exists "api_key" add column if not exists "deleted_at" timestamptz null;'
|
||||||
|
)
|
||||||
|
this.addSql(
|
||||||
|
'alter table if exists "api_key" alter column "type" type text using ("type"::text);'
|
||||||
|
)
|
||||||
|
this.addSql(
|
||||||
|
'alter table if exists "api_key" add constraint "api_key_type_check" check ("type" in (\'publishable\', \'secret\'));'
|
||||||
|
)
|
||||||
|
this.addSql(
|
||||||
|
'CREATE INDEX IF NOT EXISTS "IDX_api_key_deleted_at" ON "api_key" (deleted_at) WHERE deleted_at IS NULL;'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
'alter table if exists "api_key" drop constraint if exists "api_key_type_check";'
|
||||||
|
)
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
'alter table if exists "api_key" alter column "type" type text using ("type"::text);'
|
||||||
|
)
|
||||||
|
this.addSql('drop index if exists "IDX_api_key_deleted_at";')
|
||||||
|
this.addSql(
|
||||||
|
'alter table if exists "api_key" drop column if exists "deleted_at";'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,94 +1,26 @@
|
|||||||
import {
|
import { model } from "@medusajs/framework/utils"
|
||||||
Searchable,
|
|
||||||
createPsqlIndexStatementHelper,
|
|
||||||
generateEntityId,
|
|
||||||
} from "@medusajs/framework/utils"
|
|
||||||
|
|
||||||
import {
|
const ApiKey = model
|
||||||
BeforeCreate,
|
.define("ApiKey", {
|
||||||
Entity,
|
id: model.id({ prefix: "apk" }).primaryKey(),
|
||||||
Enum,
|
token: model.text(),
|
||||||
OnInit,
|
salt: model.text(),
|
||||||
PrimaryKey,
|
redacted: model.text().searchable(),
|
||||||
Property,
|
title: model.text().searchable(),
|
||||||
} from "@mikro-orm/core"
|
type: model.enum(["publishable", "secret"]),
|
||||||
|
last_used_at: model.dateTime().nullable(),
|
||||||
const TypeIndex = createPsqlIndexStatementHelper({
|
created_by: model.text(),
|
||||||
tableName: "api_key",
|
revoked_by: model.text().nullable(),
|
||||||
columns: "type",
|
revoked_at: model.dateTime().nullable(),
|
||||||
})
|
|
||||||
|
|
||||||
const TokenIndex = createPsqlIndexStatementHelper({
|
|
||||||
tableName: "api_key",
|
|
||||||
columns: "token",
|
|
||||||
unique: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
@Entity()
|
|
||||||
export default class ApiKey {
|
|
||||||
@PrimaryKey({ columnType: "text" })
|
|
||||||
id: string
|
|
||||||
|
|
||||||
@Property({ columnType: "text" })
|
|
||||||
@TokenIndex.MikroORMIndex()
|
|
||||||
token: string
|
|
||||||
|
|
||||||
@Property({ columnType: "text" })
|
|
||||||
salt: string
|
|
||||||
|
|
||||||
@Searchable()
|
|
||||||
@Property({ columnType: "text" })
|
|
||||||
redacted: string
|
|
||||||
|
|
||||||
@Searchable()
|
|
||||||
@Property({ columnType: "text" })
|
|
||||||
title: string
|
|
||||||
|
|
||||||
@Property({ columnType: "text" })
|
|
||||||
@Enum({ items: ["publishable", "secret"] })
|
|
||||||
@TypeIndex.MikroORMIndex()
|
|
||||||
type: "publishable" | "secret"
|
|
||||||
|
|
||||||
@Property({
|
|
||||||
columnType: "timestamptz",
|
|
||||||
nullable: true,
|
|
||||||
})
|
})
|
||||||
last_used_at: Date | null = null
|
.indexes([
|
||||||
|
{
|
||||||
|
on: ["token"],
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
on: ["type"],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
@Property({ columnType: "text" })
|
export default ApiKey
|
||||||
created_by: string
|
|
||||||
|
|
||||||
@Property({
|
|
||||||
onCreate: () => new Date(),
|
|
||||||
columnType: "timestamptz",
|
|
||||||
defaultRaw: "now()",
|
|
||||||
})
|
|
||||||
created_at: Date
|
|
||||||
|
|
||||||
@Property({
|
|
||||||
onCreate: () => new Date(),
|
|
||||||
onUpdate: () => new Date(),
|
|
||||||
columnType: "timestamptz",
|
|
||||||
defaultRaw: "now()",
|
|
||||||
})
|
|
||||||
updated_at?: Date
|
|
||||||
|
|
||||||
@Property({ columnType: "text", nullable: true })
|
|
||||||
revoked_by: string | null = null
|
|
||||||
|
|
||||||
@Property({
|
|
||||||
columnType: "timestamptz",
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
revoked_at: Date | null = null
|
|
||||||
|
|
||||||
@BeforeCreate()
|
|
||||||
onCreate() {
|
|
||||||
this.id = generateEntityId(this.id, "apk")
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnInit()
|
|
||||||
onInit() {
|
|
||||||
this.id = generateEntityId(this.id, "apk")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
FilterableApiKeyProps,
|
FilterableApiKeyProps,
|
||||||
FindConfig,
|
FindConfig,
|
||||||
IApiKeyModuleService,
|
IApiKeyModuleService,
|
||||||
|
InferEntityType,
|
||||||
InternalModuleDeclaration,
|
InternalModuleDeclaration,
|
||||||
ModuleJoinerConfig,
|
ModuleJoinerConfig,
|
||||||
ModulesSdkTypes,
|
ModulesSdkTypes,
|
||||||
@@ -46,7 +47,9 @@ export class ApiKeyModuleService
|
|||||||
implements IApiKeyModuleService
|
implements IApiKeyModuleService
|
||||||
{
|
{
|
||||||
protected baseRepository_: DAL.RepositoryService
|
protected baseRepository_: DAL.RepositoryService
|
||||||
protected readonly apiKeyService_: ModulesSdkTypes.IMedusaInternalService<ApiKey>
|
protected readonly apiKeyService_: ModulesSdkTypes.IMedusaInternalService<
|
||||||
|
InferEntityType<typeof ApiKey>
|
||||||
|
>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
{ baseRepository, apiKeyService }: InjectedDependencies,
|
{ baseRepository, apiKeyService }: InjectedDependencies,
|
||||||
@@ -138,7 +141,7 @@ export class ApiKeyModuleService
|
|||||||
protected async createApiKeys_(
|
protected async createApiKeys_(
|
||||||
data: ApiKeyTypes.CreateApiKeyDTO[],
|
data: ApiKeyTypes.CreateApiKeyDTO[],
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<[ApiKey[], TokenDTO[]]> {
|
): Promise<[InferEntityType<typeof ApiKey>[], TokenDTO[]]> {
|
||||||
await this.validateCreateApiKeys_(data, sharedContext)
|
await this.validateCreateApiKeys_(data, sharedContext)
|
||||||
|
|
||||||
const normalizedInput: CreateApiKeyDTO[] = []
|
const normalizedInput: CreateApiKeyDTO[] = []
|
||||||
@@ -276,7 +279,7 @@ export class ApiKeyModuleService
|
|||||||
protected async updateApiKeys_(
|
protected async updateApiKeys_(
|
||||||
normalizedInput: UpdateApiKeyInput[],
|
normalizedInput: UpdateApiKeyInput[],
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<ApiKey[]> {
|
): Promise<InferEntityType<typeof ApiKey>[]> {
|
||||||
const updateRequest = normalizedInput.map((k) => ({
|
const updateRequest = normalizedInput.map((k) => ({
|
||||||
id: k.id,
|
id: k.id,
|
||||||
title: k.title,
|
title: k.title,
|
||||||
@@ -387,7 +390,7 @@ export class ApiKeyModuleService
|
|||||||
async revoke_(
|
async revoke_(
|
||||||
normalizedInput: RevokeApiKeyInput[],
|
normalizedInput: RevokeApiKeyInput[],
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<ApiKey[]> {
|
): Promise<InferEntityType<typeof ApiKey>[]> {
|
||||||
await this.validateRevokeApiKeys_(normalizedInput)
|
await this.validateRevokeApiKeys_(normalizedInput)
|
||||||
|
|
||||||
const updateRequest = normalizedInput.map((k) => {
|
const updateRequest = normalizedInput.map((k) => {
|
||||||
@@ -433,7 +436,7 @@ export class ApiKeyModuleService
|
|||||||
protected async authenticate_(
|
protected async authenticate_(
|
||||||
token: string,
|
token: string,
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<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.
|
// 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.
|
// 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(
|
||||||
@@ -617,8 +620,8 @@ export class ApiKeyModuleService
|
|||||||
// 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 = (
|
const omitToken = (
|
||||||
// We have to make salt optional before deleting it (and we do want it required in the DB)
|
// We have to make salt optional before deleting it (and we do want it required in the DB)
|
||||||
key: Omit<ApiKey, "salt"> & { salt?: string }
|
key: Omit<InferEntityType<typeof ApiKey>, "salt"> & { salt?: string }
|
||||||
): Omit<ApiKey, "salt"> => {
|
): Omit<InferEntityType<typeof ApiKey>, "salt"> => {
|
||||||
key.token = key.type === ApiKeyType.SECRET ? "" : key.token
|
key.token = key.type === ApiKeyType.SECRET ? "" : key.token
|
||||||
delete key.salt
|
delete key.salt
|
||||||
return key
|
return key
|
||||||
|
|||||||
Reference in New Issue
Block a user