5
.changeset/cuddly-students-travel.md
Normal file
5
.changeset/cuddly-students-travel.md
Normal file
@@ -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",
|
||||
"tables": [
|
||||
{
|
||||
@@ -56,7 +58,11 @@
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
"enumItems": [
|
||||
"publishable",
|
||||
"secret"
|
||||
],
|
||||
"mappedType": "enum"
|
||||
},
|
||||
"last_used_at": {
|
||||
"name": "last_used_at",
|
||||
@@ -77,6 +83,25 @@
|
||||
"nullable": false,
|
||||
"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": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
@@ -99,17 +124,8 @@
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"revoked_by": {
|
||||
"name": "revoked_by",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"revoked_at": {
|
||||
"name": "revoked_at",
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
@@ -123,24 +139,34 @@
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_api_key_token_unique",
|
||||
"columnNames": ["token"],
|
||||
"keyName": "IDX_api_key_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"primary": 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",
|
||||
"columnNames": ["type"],
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"primary": 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",
|
||||
"columnNames": ["id"],
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": 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 {
|
||||
Searchable,
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
Enum,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
const TypeIndex = createPsqlIndexStatementHelper({
|
||||
tableName: "api_key",
|
||||
columns: "type",
|
||||
})
|
||||
|
||||
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,
|
||||
const ApiKey = model
|
||||
.define("ApiKey", {
|
||||
id: model.id({ prefix: "apk" }).primaryKey(),
|
||||
token: model.text(),
|
||||
salt: model.text(),
|
||||
redacted: model.text().searchable(),
|
||||
title: model.text().searchable(),
|
||||
type: model.enum(["publishable", "secret"]),
|
||||
last_used_at: model.dateTime().nullable(),
|
||||
created_by: model.text(),
|
||||
revoked_by: model.text().nullable(),
|
||||
revoked_at: model.dateTime().nullable(),
|
||||
})
|
||||
last_used_at: Date | null = null
|
||||
.indexes([
|
||||
{
|
||||
on: ["token"],
|
||||
unique: true,
|
||||
},
|
||||
{
|
||||
on: ["type"],
|
||||
},
|
||||
])
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
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")
|
||||
}
|
||||
}
|
||||
export default ApiKey
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
FilterableApiKeyProps,
|
||||
FindConfig,
|
||||
IApiKeyModuleService,
|
||||
InferEntityType,
|
||||
InternalModuleDeclaration,
|
||||
ModuleJoinerConfig,
|
||||
ModulesSdkTypes,
|
||||
@@ -46,7 +47,9 @@ export class ApiKeyModuleService
|
||||
implements IApiKeyModuleService
|
||||
{
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
protected readonly apiKeyService_: ModulesSdkTypes.IMedusaInternalService<ApiKey>
|
||||
protected readonly apiKeyService_: ModulesSdkTypes.IMedusaInternalService<
|
||||
InferEntityType<typeof ApiKey>
|
||||
>
|
||||
|
||||
constructor(
|
||||
{ baseRepository, apiKeyService }: InjectedDependencies,
|
||||
@@ -138,7 +141,7 @@ export class ApiKeyModuleService
|
||||
protected async createApiKeys_(
|
||||
data: ApiKeyTypes.CreateApiKeyDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[ApiKey[], TokenDTO[]]> {
|
||||
): Promise<[InferEntityType<typeof ApiKey>[], TokenDTO[]]> {
|
||||
await this.validateCreateApiKeys_(data, sharedContext)
|
||||
|
||||
const normalizedInput: CreateApiKeyDTO[] = []
|
||||
@@ -276,7 +279,7 @@ export class ApiKeyModuleService
|
||||
protected async updateApiKeys_(
|
||||
normalizedInput: UpdateApiKeyInput[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<ApiKey[]> {
|
||||
): Promise<InferEntityType<typeof ApiKey>[]> {
|
||||
const updateRequest = normalizedInput.map((k) => ({
|
||||
id: k.id,
|
||||
title: k.title,
|
||||
@@ -387,7 +390,7 @@ export class ApiKeyModuleService
|
||||
async revoke_(
|
||||
normalizedInput: RevokeApiKeyInput[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<ApiKey[]> {
|
||||
): Promise<InferEntityType<typeof ApiKey>[]> {
|
||||
await this.validateRevokeApiKeys_(normalizedInput)
|
||||
|
||||
const updateRequest = normalizedInput.map((k) => {
|
||||
@@ -433,7 +436,7 @@ export class ApiKeyModuleService
|
||||
protected async authenticate_(
|
||||
token: string,
|
||||
@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.
|
||||
// We can always filter on the redacted key if we add support for an arbitrary number of tokens.
|
||||
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.
|
||||
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: Omit<InferEntityType<typeof ApiKey>, "salt"> & { salt?: string }
|
||||
): Omit<InferEntityType<typeof ApiKey>, "salt"> => {
|
||||
key.token = key.type === ApiKeyType.SECRET ? "" : key.token
|
||||
delete key.salt
|
||||
return key
|
||||
|
||||
Reference in New Issue
Block a user