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
- .batch(storage_instance, &block) ⇒ Object
- .batch_migrate_legacy_metadata(ctx, records, config) ⇒ Object
- .delete(ctx, record, config) ⇒ Object
- .delete_record(ctx, record, config) ⇒ Object
- .deserialize_record(record) ⇒ Object
- .find_by_hash(ctx, hashed, config) ⇒ Object
- .find_by_id(ctx, id, config) ⇒ Object
- .get(ctx, key, config) ⇒ Object
- .legacy_metadata_migration_needed?(record) ⇒ Boolean
- .list_for_reference(ctx, reference_id, config) ⇒ Object
- .migrate_legacy_metadata(ctx, record, config) ⇒ Object
- .parse_id_list!(raw) ⇒ Object
- .populate_reference(ctx, reference_id, records, config) ⇒ Object
- .ref_list_add(storage_instance, reference_key, id) ⇒ Object
- .ref_list_remove(storage_instance, reference_key, id) ⇒ Object
- .safe_parse_id_list(raw) ⇒ Object
- .schedule_record_delete(ctx, record, config) ⇒ Object
- .set(ctx, record, config) ⇒ Object
- .storage(config, context = nil) ⇒ Object
- .storage_key_by_hash(hashed_key) ⇒ Object
- .storage_key_by_id(id) ⇒ Object
- .storage_key_by_reference(reference_id) ⇒ Object
- .storage_record(record) ⇒ Object
- .store(ctx, data, config) ⇒ Object
- .update_record(ctx, record, update, config, defer: false) ⇒ Object
Class Method Details
.batch(storage_instance, &block) ⇒ Object
227 228 229 230 231 232 233 |
# File 'lib/better_auth/api_key/adapter.rb', line 227 def batch(storage_instance, &block) if storage_instance.respond_to?(:batch) storage_instance.batch(&block) else block.call end end |
.batch_migrate_legacy_metadata(ctx, records, config) ⇒ Object
254 255 256 257 258 |
# File 'lib/better_auth/api_key/adapter.rb', line 254 def (ctx, records, config) return records unless config[:storage] == "database" || config[:fallback_to_database] records.map { |record| (ctx, record, config) } end |
.delete(ctx, record, config) ⇒ Object
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/better_auth/api_key/adapter.rb', line 175 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
104 105 106 107 |
# File 'lib/better_auth/api_key/adapter.rb', line 104 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
264 265 266 267 268 269 |
# File 'lib/better_auth/api_key/adapter.rb', line 264 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
142 143 144 145 146 147 |
# File 'lib/better_auth/api_key/adapter.rb', line 142 def get(ctx, key, config) raw = storage(config, ctx.context)&.get(key) raw && deserialize_record(JSON.parse(raw)) rescue JSON::ParserError nil end |
.legacy_metadata_migration_needed?(record) ⇒ Boolean
133 134 135 136 |
# File 'lib/better_auth/api_key/adapter.rb', line 133 def (record) parsed = BetterAuth::APIKey::Utils.decode_json(record["metadata"]) parsed.is_a?(Hash) && BetterAuth::APIKey::Utils.encode_json(parsed) != record["metadata"] 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 77 78 79 80 |
# 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) raw_ids = storage_instance&.get(storage_key_by_reference(reference_id)) || storage_instance&.get("api-key:user:#{reference_id}") ids = parse_id_list!(raw_ids) records = ids.filter_map { |id| find_by_id(ctx, id, config) } return records unless records.empty? && config[:fallback_to_database] rescue JSON::ParserError, NoMethodError => error if ctx.context.respond_to?(:logger) && ctx.context.logger.respond_to?(:warn) ctx.context.logger.warn("[API KEY PLUGIN] Corrupt api-key reference index for #{reference_id.inspect}: #{error.class}: #{error.}") end 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
118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/better_auth/api_key/adapter.rb', line 118 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 |
.parse_id_list!(raw) ⇒ Object
219 220 221 222 223 224 225 |
# File 'lib/better_auth/api_key/adapter.rb', line 219 def parse_id_list!(raw) return [] if raw.nil? return raw.dup if raw.is_a?(Array) parsed = JSON.parse(raw.to_s) parsed.is_a?(Array) ? parsed : [] end |
.populate_reference(ctx, reference_id, records, config) ⇒ Object
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 |
# File 'lib/better_auth/api_key/adapter.rb', line 235 def populate_reference(ctx, reference_id, records, config) storage_instance = storage(config, ctx.context) return unless storage_instance batch(storage_instance) do 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 end |
.ref_list_add(storage_instance, reference_key, id) ⇒ Object
199 200 201 202 203 |
# File 'lib/better_auth/api_key/adapter.rb', line 199 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
205 206 207 208 |
# File 'lib/better_auth/api_key/adapter.rb', line 205 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
210 211 212 213 214 215 216 217 |
# File 'lib/better_auth/api_key/adapter.rb', line 210 def safe_parse_id_list(raw) return [] if raw.nil? return raw.dup if raw.is_a?(Array) parse_id_list!(raw) rescue JSON::ParserError [] end |
.schedule_record_delete(ctx, record, config) ⇒ Object
109 110 111 112 113 114 115 116 |
# File 'lib/better_auth/api_key/adapter.rb', line 109 def schedule_record_delete(ctx, record, config) task = -> { delete_record(ctx, record, config) } if config[:defer_updates] && BetterAuth::APIKey::Utils.background_tasks?(ctx) BetterAuth::APIKey::Utils.run_background_task(ctx, "Deferred API key delete", task) else task.call end end |
.set(ctx, record, config) ⇒ Object
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/better_auth/api_key/adapter.rb', line 149 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
138 139 140 |
# File 'lib/better_auth/api_key/adapter.rb', line 138 def storage(config, context = nil) config[:custom_storage] || context&.&.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
260 261 262 |
# File 'lib/better_auth/api_key/adapter.rb', line 260 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
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/better_auth/api_key/adapter.rb', line 82 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) return nil unless updated else updated = record.merge(update.transform_keys { |key| BetterAuth::Schema.storage_key(key) }) end 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) }) BetterAuth::APIKey::Utils.run_background_task(ctx, "Deferred API key update", performer) scheduled else performer.call end end |