Module: BetterAuth::APIKey::Adapter

Defined in:
lib/better_auth/api_key/adapter.rb

Constant Summary collapse

HASH_STORAGE_PREFIX =
"api-key:"
ID_STORAGE_PREFIX =
"api-key:by-id:"
REFERENCE_STORAGE_PREFIX =
"api-key:by-ref:"

Class Method Summary collapse

Class Method Details

.batch(storage_instance, &block) ⇒ Object



208
209
210
211
212
213
214
# File 'lib/better_auth/api_key/adapter.rb', line 208

def batch(storage_instance, &block)
  if storage_instance.respond_to?(:batch)
    storage_instance.batch(&block)
  else
    block.call
  end
end

.delete(ctx, record, config) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/better_auth/api_key/adapter.rb', line 164

def delete(ctx, record, config)
  storage_instance = storage(config, ctx.context)
  return unless storage_instance

  reference_id = BetterAuth::Plugins.api_key_record_reference_id(record)
  reference_key = storage_key_by_reference(reference_id)

  batch(storage_instance) do
    operations = [
      -> { storage_instance.delete(storage_key_by_hash(record["key"])) },
      -> { storage_instance.delete(storage_key_by_id(record["id"])) },
      # Ruby-only legacy storage layout cleanup; upstream never wrote here.
      -> { storage_instance.delete("api-key:key:#{record["key"]}") },
      -> { storage_instance.delete("api-key:id:#{record["id"]}") }
    ]
    operations << if config[:fallback_to_database]
      -> { storage_instance.delete(reference_key) }
    else
      -> { ref_list_remove(storage_instance, reference_key, record["id"]) }
    end
    operations.each(&:call)
  end
end

.delete_record(ctx, record, config) ⇒ Object



98
99
100
101
# File 'lib/better_auth/api_key/adapter.rb', line 98

def delete_record(ctx, record, config)
  ctx.context.adapter.delete(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: [{field: "id", value: record["id"]}]) if config[:storage] == "database" || config[:fallback_to_database]
  delete(ctx, record, config) if config[:storage] == "secondary-storage"
end

.deserialize_record(record) ⇒ Object



237
238
239
240
241
242
# File 'lib/better_auth/api_key/adapter.rb', line 237

def deserialize_record(record)
  %w[createdAt updatedAt expiresAt lastRefillAt lastRequest].each do |field|
    record[field] = BetterAuth::APIKey::Utils.normalize_time(record[field]) if record[field]
  end
  record
end

.find_by_hash(ctx, hashed, config) ⇒ Object



38
39
40
41
42
43
44
45
46
47
# File 'lib/better_auth/api_key/adapter.rb', line 38

def find_by_hash(ctx, hashed, config)
  if config[:storage] == "secondary-storage"
    record = get(ctx, storage_key_by_hash(hashed), config) || get(ctx, "api-key:key:#{hashed}", config)
    return record if record
    return nil unless config[:fallback_to_database]
  end
  record = ctx.context.adapter.find_one(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: [{field: "key", value: hashed}])
  set(ctx, record, config) if record && config[:storage] == "secondary-storage" && config[:fallback_to_database]
  record
end

.find_by_id(ctx, id, config) ⇒ Object



49
50
51
52
53
54
55
56
57
58
# File 'lib/better_auth/api_key/adapter.rb', line 49

def find_by_id(ctx, id, config)
  if config[:storage] == "secondary-storage"
    record = get(ctx, storage_key_by_id(id), config) || get(ctx, "api-key:id:#{id}", config)
    return record if record
    return nil unless config[:fallback_to_database]
  end
  record = ctx.context.adapter.find_one(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: [{field: "id", value: id}])
  set(ctx, record, config) if record && config[:storage] == "secondary-storage" && config[:fallback_to_database]
  record
end

.get(ctx, key, config) ⇒ Object



131
132
133
134
135
136
# File 'lib/better_auth/api_key/adapter.rb', line 131

def get(ctx, key, config)
  raw = storage(config, ctx.context)&.get(key)
  raw && deserialize_record(JSON.parse(raw))
rescue JSON::ParserError
  nil
end

.list_for_reference(ctx, reference_id, config) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/better_auth/api_key/adapter.rb', line 60

def list_for_reference(ctx, reference_id, config)
  if config[:storage] == "secondary-storage"
    begin
      storage_instance = storage(config, ctx.context)
      ids = JSON.parse((storage_instance&.get(storage_key_by_reference(reference_id)) || storage_instance&.get("api-key:user:#{reference_id}")).to_s)
      records = ids.filter_map { |id| find_by_id(ctx, id, config) }
      return records unless records.empty? && config[:fallback_to_database]
    rescue JSON::ParserError, NoMethodError
      return [] unless config[:fallback_to_database]
    end
  end
  records = ctx.context.adapter.find_many(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: [{field: "referenceId", value: reference_id}])
  legacy = ctx.context.adapter.find_many(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: [{field: "userId", value: reference_id}])
  combined = (records + legacy).uniq { |record| record["id"] }
  populate_reference(ctx, reference_id, combined, config) if config[:storage] == "secondary-storage" && config[:fallback_to_database]
  combined
end

.migrate_legacy_metadata(ctx, record, config) ⇒ Object



112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/better_auth/api_key/adapter.rb', line 112

def (ctx, record, config)
  parsed = BetterAuth::APIKey::Utils.decode_json(record["metadata"])
  return record unless parsed.is_a?(Hash)

  encoded = BetterAuth::APIKey::Utils.encode_json(parsed)
  return record.merge("metadata" => encoded) if record["metadata"] == encoded

  updated = record.merge("metadata" => encoded)
  if config[:storage] == "database" || config[:fallback_to_database]
    ctx.context.adapter.update(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: [{field: "id", value: record["id"]}], update: {metadata: encoded})
  end
  set(ctx, updated, config) if config[:storage] == "secondary-storage"
  updated
end

.populate_reference(ctx, reference_id, records, config) ⇒ Object



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/better_auth/api_key/adapter.rb', line 216

def populate_reference(ctx, reference_id, records, config)
  storage_instance = storage(config, ctx.context)
  return unless storage_instance

  ids = []
  records.each do |record|
    serialized = JSON.generate(storage_record(record))
    expires_at = BetterAuth::APIKey::Utils.normalize_time(record["expiresAt"])
    ttl = expires_at ? [(expires_at - Time.now).to_i, 0].max : nil
    storage_instance.set(storage_key_by_hash(record["key"]), serialized, ttl)
    storage_instance.set(storage_key_by_id(record["id"]), serialized, ttl)
    ids << record["id"]
  end
  reference_key = storage_key_by_reference(reference_id)
  ids.empty? ? storage_instance.delete(reference_key) : storage_instance.set(reference_key, JSON.generate(ids))
end

.ref_list_add(storage_instance, reference_key, id) ⇒ Object



188
189
190
191
192
# File 'lib/better_auth/api_key/adapter.rb', line 188

def ref_list_add(storage_instance, reference_key, id)
  ids = safe_parse_id_list(storage_instance.get(reference_key))
  ids << id unless ids.include?(id)
  storage_instance.set(reference_key, JSON.generate(ids))
end

.ref_list_remove(storage_instance, reference_key, id) ⇒ Object



194
195
196
197
# File 'lib/better_auth/api_key/adapter.rb', line 194

def ref_list_remove(storage_instance, reference_key, id)
  ids = safe_parse_id_list(storage_instance.get(reference_key)).reject { |existing| existing == id }
  ids.empty? ? storage_instance.delete(reference_key) : storage_instance.set(reference_key, JSON.generate(ids))
end

.safe_parse_id_list(raw) ⇒ Object



199
200
201
202
203
204
205
206
# File 'lib/better_auth/api_key/adapter.rb', line 199

def safe_parse_id_list(raw)
  return [] if raw.nil?

  parsed = JSON.parse(raw.to_s)
  parsed.is_a?(Array) ? parsed : []
rescue JSON::ParserError
  []
end

.schedule_record_delete(ctx, record, config) ⇒ Object



103
104
105
106
107
108
109
110
# File 'lib/better_auth/api_key/adapter.rb', line 103

def schedule_record_delete(ctx, record, config)
  task = -> { delete_record(ctx, record, config) }
  if config[:defer_updates] && BetterAuth::APIKey::Utils.background_tasks?(ctx)
    ctx.context.run_in_background(task)
  else
    task.call
  end
end

.set(ctx, record, config) ⇒ Object



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/better_auth/api_key/adapter.rb', line 138

def set(ctx, record, config)
  storage_instance = storage(config, ctx.context)
  unless storage_instance
    raise BetterAuth::APIError.new("INTERNAL_SERVER_ERROR", message: "Secondary storage is required when storage mode is 'secondary-storage'")
  end

  serialized = JSON.generate(storage_record(record))
  expires_at = BetterAuth::APIKey::Utils.normalize_time(record["expiresAt"])
  ttl = expires_at ? [(expires_at - Time.now).to_i, 0].max : nil
  reference_id = BetterAuth::Plugins.api_key_record_reference_id(record)
  reference_key = storage_key_by_reference(reference_id)

  batch(storage_instance) do
    operations = [
      -> { storage_instance.set(storage_key_by_hash(record["key"]), serialized, ttl) },
      -> { storage_instance.set(storage_key_by_id(record["id"]), serialized, ttl) }
    ]
    operations << if config[:fallback_to_database]
      -> { storage_instance.delete(reference_key) }
    else
      -> { ref_list_add(storage_instance, reference_key, record["id"]) }
    end
    operations.each(&:call)
  end
end

.storage(config, context = nil) ⇒ Object



127
128
129
# File 'lib/better_auth/api_key/adapter.rb', line 127

def storage(config, context = nil)
  config[:custom_storage] || context&.options&.secondary_storage
end

.storage_key_by_hash(hashed_key) ⇒ Object



16
17
18
# File 'lib/better_auth/api_key/adapter.rb', line 16

def storage_key_by_hash(hashed_key)
  "#{HASH_STORAGE_PREFIX}#{hashed_key}"
end

.storage_key_by_id(id) ⇒ Object



20
21
22
# File 'lib/better_auth/api_key/adapter.rb', line 20

def storage_key_by_id(id)
  "#{ID_STORAGE_PREFIX}#{id}"
end

.storage_key_by_reference(reference_id) ⇒ Object



24
25
26
# File 'lib/better_auth/api_key/adapter.rb', line 24

def storage_key_by_reference(reference_id)
  "#{REFERENCE_STORAGE_PREFIX}#{reference_id}"
end

.storage_record(record) ⇒ Object



233
234
235
# File 'lib/better_auth/api_key/adapter.rb', line 233

def storage_record(record)
  record.transform_values { |value| value.is_a?(Time) ? value.iso8601 : value }
end

.store(ctx, data, config) ⇒ Object



28
29
30
31
32
33
34
35
36
# File 'lib/better_auth/api_key/adapter.rb', line 28

def store(ctx, data, config)
  record = nil
  if config[:storage] == "database" || config[:fallback_to_database]
    record = ctx.context.adapter.create(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, data: data)
  end
  record ||= data.transform_keys { |key| BetterAuth::Schema.storage_key(key) }.merge("id" => SecureRandom.hex(16))
  set(ctx, record, config) if config[:storage] == "secondary-storage"
  record
end

.update_record(ctx, record, update, config, defer: false) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/better_auth/api_key/adapter.rb', line 78

def update_record(ctx, record, update, config, defer: false)
  performer = lambda do
    updated = nil
    if config[:storage] == "database" || config[:fallback_to_database]
      updated = ctx.context.adapter.update(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: [{field: "id", value: record["id"]}], update: update)
    end
    updated ||= record.merge(update.transform_keys { |key| BetterAuth::Schema.storage_key(key) })
    set(ctx, updated, config) if config[:storage] == "secondary-storage"
    updated
  end

  if defer && config[:defer_updates] && BetterAuth::Plugins.api_key_background_tasks?(ctx)
    scheduled = record.merge(update.transform_keys { |key| BetterAuth::Schema.storage_key(key) })
    ctx.context.run_in_background(performer)
    scheduled
  else
    performer.call
  end
end