api_keys 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +60 -0
- data/README.md +851 -25
- data/SECURITY.md +33 -0
- data/app/controllers/api_keys/application_controller.rb +58 -10
- data/app/controllers/api_keys/keys_controller.rb +77 -23
- data/app/controllers/api_keys/security_controller.rb +8 -0
- data/app/views/api_keys/keys/_empty_state.html.erb +9 -0
- data/app/views/api_keys/keys/_form.html.erb +33 -4
- data/app/views/api_keys/keys/_key_actions.html.erb +20 -0
- data/app/views/api_keys/keys/_key_badges.html.erb +17 -0
- data/app/views/api_keys/keys/_key_row.html.erb +21 -35
- data/app/views/api_keys/keys/_key_status.html.erb +10 -0
- data/app/views/api_keys/keys/_keys_table.html.erb +3 -11
- data/app/views/api_keys/keys/_publishable_keys.html.erb +40 -0
- data/app/views/api_keys/keys/_secret_keys.html.erb +39 -0
- data/app/views/api_keys/keys/_show_token.html.erb +10 -47
- data/app/views/api_keys/keys/_token_display.html.erb +11 -0
- data/app/views/api_keys/keys/index.html.erb +40 -8
- data/app/views/api_keys/keys/show.html.erb +2 -2
- data/app/views/api_keys/security/best_practices.html.erb +73 -47
- data/app/views/layouts/api_keys/application.html.erb +267 -14
- data/lib/api_keys/authentication.rb +39 -11
- data/lib/api_keys/configuration.rb +444 -17
- data/lib/api_keys/engine.rb +5 -20
- data/lib/api_keys/errors.rb +73 -0
- data/lib/api_keys/form_builder_extensions.rb +168 -0
- data/lib/api_keys/helpers/expiration_options.rb +139 -0
- data/lib/api_keys/helpers/token_session.rb +203 -0
- data/lib/api_keys/helpers/view_helpers.rb +220 -0
- data/lib/api_keys/jobs/callbacks_job.rb +10 -17
- data/lib/api_keys/jobs/update_stats_job.rb +27 -12
- data/lib/api_keys/models/api_key.rb +452 -21
- data/lib/api_keys/models/concerns/has_api_keys.rb +269 -26
- data/lib/api_keys/services/authenticator.rb +300 -112
- data/lib/api_keys/services/digestor.rb +81 -14
- data/lib/api_keys/services/token_generator.rb +41 -1
- data/lib/api_keys/tenant_resolution.rb +4 -4
- data/lib/api_keys/version.rb +1 -1
- data/lib/api_keys.rb +12 -0
- data/lib/generators/api_keys/add_authentication_index_generator.rb +36 -0
- data/lib/generators/api_keys/add_key_types_generator.rb +68 -0
- data/lib/generators/api_keys/templates/add_authentication_index_to_api_keys.rb.erb +32 -0
- data/lib/generators/api_keys/templates/add_key_types_to_api_keys.rb.erb +18 -0
- data/lib/generators/api_keys/templates/create_api_keys_table.rb.erb +11 -3
- data/lib/generators/api_keys/templates/initializer.rb +261 -120
- metadata +29 -63
- data/Rakefile +0 -32
|
@@ -4,23 +4,159 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>🔑 API Keys</title>
|
|
7
|
-
<%#
|
|
8
|
-
<link rel="stylesheet" href="https://unpkg.com/chota@latest">
|
|
9
|
-
<%# Host application is expected to provide styling %>
|
|
7
|
+
<%# Self-contained styles: do not load mutable third-party assets on secret-bearing pages. %>
|
|
10
8
|
<%= csrf_meta_tags %>
|
|
11
9
|
<%= csp_meta_tag %>
|
|
12
10
|
</head>
|
|
13
11
|
<body>
|
|
14
|
-
<style>
|
|
12
|
+
<style nonce="<%= content_security_policy_nonce %>">
|
|
13
|
+
/*
|
|
14
|
+
* API Keys CSS Variables
|
|
15
|
+
* Override these in your host application to customize the dashboard appearance.
|
|
16
|
+
* Example in your app's CSS:
|
|
17
|
+
* :root {
|
|
18
|
+
* --api-keys-primary-color: #your-brand-color;
|
|
19
|
+
* --api-keys-danger-color: #your-danger-color;
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
:root {
|
|
23
|
+
/* Colors */
|
|
24
|
+
--api-keys-primary-color: #007bff;
|
|
25
|
+
--api-keys-danger-color: #c23539;
|
|
26
|
+
--api-keys-success-color: #28a745;
|
|
27
|
+
--api-keys-warning-color: #ffc107;
|
|
28
|
+
--api-keys-muted-color: #6c757d;
|
|
29
|
+
|
|
30
|
+
/* Badge colors */
|
|
31
|
+
--api-keys-badge-secret-bg: #e7f1ff;
|
|
32
|
+
--api-keys-badge-secret-color: #004085;
|
|
33
|
+
--api-keys-badge-publishable-bg: #fef3cd;
|
|
34
|
+
--api-keys-badge-publishable-color: #856404;
|
|
35
|
+
--api-keys-badge-live-bg: #d4edda;
|
|
36
|
+
--api-keys-badge-live-color: #155724;
|
|
37
|
+
--api-keys-badge-test-bg: #f8d7da;
|
|
38
|
+
--api-keys-badge-test-color: #721c24;
|
|
39
|
+
|
|
40
|
+
/* Status colors */
|
|
41
|
+
--api-keys-status-active-color: green;
|
|
42
|
+
--api-keys-status-revoked-color: orange;
|
|
43
|
+
--api-keys-status-expired-color: red;
|
|
44
|
+
|
|
45
|
+
/* Spacing */
|
|
46
|
+
--api-keys-section-padding: 1.5em;
|
|
47
|
+
--api-keys-section-margin: 2em;
|
|
48
|
+
--api-keys-border-radius: 8px;
|
|
49
|
+
|
|
50
|
+
/* Typography */
|
|
51
|
+
--api-keys-font-family: inherit;
|
|
52
|
+
--api-keys-code-font-size: 0.8em;
|
|
53
|
+
}
|
|
54
|
+
|
|
15
55
|
body {
|
|
16
|
-
|
|
56
|
+
font-family: var(--api-keys-font-family);
|
|
57
|
+
margin: 0;
|
|
58
|
+
color: var(--font-color, #222);
|
|
59
|
+
background: var(--bg-color, #fff);
|
|
60
|
+
line-height: 1.5;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.container {
|
|
64
|
+
width: min(94%, 1080px);
|
|
65
|
+
margin-inline: auto;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.nav, .row {
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
gap: 1rem;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.nav {
|
|
75
|
+
justify-content: space-between;
|
|
76
|
+
min-height: 4rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.nav-left, .nav-right {
|
|
80
|
+
display: flex;
|
|
81
|
+
gap: 1rem;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.col { flex: 1; }
|
|
85
|
+
.col-5 { width: min(100%, 42rem); }
|
|
86
|
+
.col-7 { flex: 0 0 58%; }
|
|
87
|
+
.col-8 { width: min(100%, 54rem); }
|
|
88
|
+
.is-right { justify-content: flex-end; }
|
|
89
|
+
.is-center { justify-content: center; }
|
|
90
|
+
.text-center { text-align: center; }
|
|
91
|
+
|
|
92
|
+
input, select, button, .button {
|
|
93
|
+
box-sizing: border-box;
|
|
94
|
+
font: inherit;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
input[type="text"], select {
|
|
98
|
+
width: 100%;
|
|
99
|
+
padding: 0.65rem;
|
|
100
|
+
border: 1px solid var(--color-grey, #bbb);
|
|
101
|
+
border-radius: 4px;
|
|
102
|
+
background: var(--bg-color, #fff);
|
|
103
|
+
color: inherit;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
button, .button {
|
|
107
|
+
display: inline-block;
|
|
108
|
+
padding: 0.55rem 0.85rem;
|
|
109
|
+
border: 1px solid currentColor;
|
|
110
|
+
border-radius: 4px;
|
|
111
|
+
cursor: pointer;
|
|
112
|
+
text-decoration: none;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.primary {
|
|
116
|
+
color: #fff;
|
|
117
|
+
background: var(--api-keys-primary-color);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.card {
|
|
121
|
+
padding: 1rem;
|
|
122
|
+
border: 1px solid var(--color-grey, #ccc);
|
|
123
|
+
border-radius: var(--api-keys-border-radius);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
table {
|
|
127
|
+
width: 100%;
|
|
128
|
+
border-collapse: collapse;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
th, td {
|
|
132
|
+
padding: 0.65rem;
|
|
133
|
+
border-bottom: 1px solid var(--color-grey, #ddd);
|
|
134
|
+
text-align: left;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.api-keys-table-wrapper { overflow-x: auto; }
|
|
138
|
+
.tag, .api-keys-badge {
|
|
139
|
+
display: inline-block;
|
|
140
|
+
padding: 0.15rem 0.4rem;
|
|
141
|
+
border-radius: 999px;
|
|
142
|
+
background: var(--bg-secondary-color, #eee);
|
|
17
143
|
}
|
|
18
144
|
|
|
19
145
|
@media (prefers-color-scheme: dark) {
|
|
146
|
+
:root {
|
|
147
|
+
--api-keys-badge-secret-bg: #1a365d;
|
|
148
|
+
--api-keys-badge-secret-color: #90cdf4;
|
|
149
|
+
--api-keys-badge-publishable-bg: #744210;
|
|
150
|
+
--api-keys-badge-publishable-color: #faf089;
|
|
151
|
+
--api-keys-badge-live-bg: #22543d;
|
|
152
|
+
--api-keys-badge-live-color: #9ae6b4;
|
|
153
|
+
--api-keys-badge-test-bg: #742a2a;
|
|
154
|
+
--api-keys-badge-test-color: #feb2b2;
|
|
155
|
+
}
|
|
156
|
+
|
|
20
157
|
body {
|
|
21
|
-
|
|
22
|
-
--bg-color:rgb(
|
|
23
|
-
--bg-secondary-color:rgb(34, 34, 34);
|
|
158
|
+
--bg-color: rgb(14, 14, 14);
|
|
159
|
+
--bg-secondary-color: rgb(34, 34, 34);
|
|
24
160
|
--font-color: #f5f5f5;
|
|
25
161
|
--color-grey: #ccc;
|
|
26
162
|
--color-darkGrey: #777;
|
|
@@ -29,7 +165,7 @@
|
|
|
29
165
|
|
|
30
166
|
code, pre {
|
|
31
167
|
color: var(--font-color);
|
|
32
|
-
font-size:
|
|
168
|
+
font-size: var(--api-keys-code-font-size);
|
|
33
169
|
}
|
|
34
170
|
|
|
35
171
|
.api-keys-align-center {
|
|
@@ -52,7 +188,51 @@
|
|
|
52
188
|
.api-keys-action-buttons button {
|
|
53
189
|
background: none;
|
|
54
190
|
padding: 0;
|
|
55
|
-
color:
|
|
191
|
+
color: var(--api-keys-danger-color);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
[hidden] { display: none !important; }
|
|
195
|
+
.api-keys-form-errors { color: var(--api-keys-danger-color); }
|
|
196
|
+
.api-keys-form-help { display: block; color: var(--color-darkGrey, #666); }
|
|
197
|
+
.api-keys-empty-state { text-align: center; padding: 2em; }
|
|
198
|
+
.api-keys-expired-text { color: var(--api-keys-status-expired-color); }
|
|
199
|
+
.api-keys-token-panel { padding: 1em; margin: 1em 0; border-radius: 4px; }
|
|
200
|
+
.api-keys-token-value { overflow-wrap: anywhere; }
|
|
201
|
+
.api-keys-token-scopes, .api-keys-show-footer { margin-top: 2em; }
|
|
202
|
+
.api-keys-token-break { overflow-wrap: anywhere; }
|
|
203
|
+
.btn-show-token, .btn-copy-token {
|
|
204
|
+
margin-left: 0.5rem;
|
|
205
|
+
padding: 0.15rem 0.4rem;
|
|
206
|
+
font-size: 0.75em;
|
|
207
|
+
}
|
|
208
|
+
.api-keys-action-disabled {
|
|
209
|
+
color: var(--api-keys-muted-color);
|
|
210
|
+
cursor: help;
|
|
211
|
+
}
|
|
212
|
+
.api-keys-status-active { color: var(--api-keys-status-active-color); }
|
|
213
|
+
.api-keys-status-revoked { color: var(--api-keys-status-revoked-color); }
|
|
214
|
+
.api-keys-status-expired { color: var(--api-keys-status-expired-color); }
|
|
215
|
+
.api-keys-badge-type, .api-keys-badge-env {
|
|
216
|
+
margin-left: 0.25rem;
|
|
217
|
+
padding: 0.15rem 0.4rem;
|
|
218
|
+
border-radius: 3px;
|
|
219
|
+
font-size: 0.75em;
|
|
220
|
+
}
|
|
221
|
+
.api-keys-badge-publishable {
|
|
222
|
+
color: var(--api-keys-badge-publishable-color);
|
|
223
|
+
background-color: var(--api-keys-badge-publishable-bg);
|
|
224
|
+
}
|
|
225
|
+
.api-keys-badge-secret {
|
|
226
|
+
color: var(--api-keys-badge-secret-color);
|
|
227
|
+
background-color: var(--api-keys-badge-secret-bg);
|
|
228
|
+
}
|
|
229
|
+
.api-keys-badge-live {
|
|
230
|
+
color: var(--api-keys-badge-live-color);
|
|
231
|
+
background-color: var(--api-keys-badge-live-bg);
|
|
232
|
+
}
|
|
233
|
+
.api-keys-badge-test {
|
|
234
|
+
color: var(--api-keys-badge-test-color);
|
|
235
|
+
background-color: var(--api-keys-badge-test-bg);
|
|
56
236
|
}
|
|
57
237
|
|
|
58
238
|
.api-keys-button-text {
|
|
@@ -62,7 +242,7 @@
|
|
|
62
242
|
#api-keys-flash-container {
|
|
63
243
|
margin-top: 1em;
|
|
64
244
|
}
|
|
65
|
-
|
|
245
|
+
|
|
66
246
|
.api-keys-flash-message {
|
|
67
247
|
background-color: rgba(255, 255, 255, 0.15);
|
|
68
248
|
padding: 0.8em 0;
|
|
@@ -77,6 +257,38 @@
|
|
|
77
257
|
margin-top: 2.4em;
|
|
78
258
|
}
|
|
79
259
|
|
|
260
|
+
/* API Keys sections styling */
|
|
261
|
+
.api-keys-section {
|
|
262
|
+
margin-top: var(--api-keys-section-margin);
|
|
263
|
+
padding: var(--api-keys-section-padding);
|
|
264
|
+
border: 1px solid var(--color-grey, #ccc);
|
|
265
|
+
border-radius: var(--api-keys-border-radius);
|
|
266
|
+
background-color: var(--bg-secondary-color, #f9f9f9);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.api-keys-section h2 {
|
|
270
|
+
margin-top: 0;
|
|
271
|
+
margin-bottom: 0.5em;
|
|
272
|
+
font-size: 1.25em;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.api-keys-section-description {
|
|
276
|
+
margin-bottom: 1em;
|
|
277
|
+
color: var(--color-darkGrey, #666);
|
|
278
|
+
font-size: 0.9em;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.api-keys-info-text {
|
|
282
|
+
margin-bottom: 1em;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
@media (prefers-color-scheme: dark) {
|
|
286
|
+
.api-keys-section {
|
|
287
|
+
background-color: var(--bg-secondary-color, rgb(34, 34, 34));
|
|
288
|
+
border-color: var(--color-darkGrey, #555);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
80
292
|
</style>
|
|
81
293
|
|
|
82
294
|
<nav class="nav container">
|
|
@@ -84,7 +296,7 @@
|
|
|
84
296
|
<%# Use configured return URL and text %>
|
|
85
297
|
<%= link_to ApiKeys.configuration.return_text, ApiKeys.configuration.return_url %>
|
|
86
298
|
<%# Link to keys index using engine's path helper %>
|
|
87
|
-
<%= link_to "My API Keys",
|
|
299
|
+
<%= link_to "My API Keys", keys_path, class: "active" %>
|
|
88
300
|
</div>
|
|
89
301
|
<div class="nav-center">
|
|
90
302
|
</div>
|
|
@@ -109,7 +321,48 @@
|
|
|
109
321
|
<%# Footer content %>
|
|
110
322
|
</footer>
|
|
111
323
|
|
|
112
|
-
|
|
324
|
+
<script nonce="<%= content_security_policy_nonce %>">
|
|
325
|
+
// Minimal JS for API Keys dashboard - event delegation for show/copy tokens
|
|
326
|
+
document.addEventListener('click', function(e) {
|
|
327
|
+
// Show token (one-way: masked -> full, no hiding back)
|
|
328
|
+
if (e.target.matches('.btn-show-token')) {
|
|
329
|
+
var cell = e.target.closest('td');
|
|
330
|
+
cell.querySelector('.token-masked').hidden = true;
|
|
331
|
+
cell.querySelector('.token-full').hidden = false;
|
|
332
|
+
e.target.hidden = true;
|
|
333
|
+
cell.querySelector('.btn-copy-token').hidden = false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Copy token to clipboard
|
|
337
|
+
if (e.target.matches('.btn-copy-token')) {
|
|
338
|
+
var token = e.target.getAttribute('data-token');
|
|
339
|
+
navigator.clipboard.writeText(token).then(function() {
|
|
340
|
+
var originalText = e.target.textContent;
|
|
341
|
+
e.target.textContent = 'Copied!';
|
|
342
|
+
setTimeout(function() { e.target.textContent = originalText; }, 2000);
|
|
343
|
+
}).catch(function() {
|
|
344
|
+
e.target.textContent = 'Copy failed';
|
|
345
|
+
setTimeout(function() { e.target.textContent = 'Copy'; }, 2000);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Copy a newly issued secret without duplicating it into an HTML attribute.
|
|
350
|
+
if (e.target.closest('.btn-copy-new-token')) {
|
|
351
|
+
var button = e.target.closest('.btn-copy-new-token');
|
|
352
|
+
var tokenElement = document.getElementById('api-key-token');
|
|
353
|
+
var buttonText = button.querySelector('.api-keys-button-text');
|
|
354
|
+
if (!navigator.clipboard || !tokenElement || !buttonText) return;
|
|
355
|
+
|
|
356
|
+
navigator.clipboard.writeText(tokenElement.textContent || '').then(function() {
|
|
357
|
+
buttonText.textContent = 'Copied!';
|
|
358
|
+
setTimeout(function() { buttonText.textContent = 'Copy'; }, 2000);
|
|
359
|
+
}).catch(function() {
|
|
360
|
+
buttonText.textContent = 'Copy failed';
|
|
361
|
+
setTimeout(function() { buttonText.textContent = 'Copy'; }, 2000);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
</script>
|
|
113
366
|
|
|
114
367
|
</body>
|
|
115
|
-
</html>
|
|
368
|
+
</html>
|
|
@@ -32,17 +32,21 @@ module ApiKeys
|
|
|
32
32
|
current_api_key&.owner
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
alias_method :current_api_key_owner, :current_api_owner
|
|
36
|
+
|
|
35
37
|
# Convenience helper: returns the owner if it's a User instance.
|
|
36
38
|
# @return [User, nil]
|
|
37
39
|
def current_api_user
|
|
38
40
|
owner = current_api_owner
|
|
39
|
-
owner if owner.is_a?(::User)
|
|
41
|
+
owner if defined?(::User) && owner.is_a?(::User)
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
private
|
|
43
45
|
|
|
44
46
|
# The core authentication method.
|
|
45
47
|
def authenticate_api_key!(scope: nil)
|
|
48
|
+
@current_api_key = nil
|
|
49
|
+
remove_instance_variable(:@current_api_tenant) if instance_variable_defined?(:@current_api_tenant)
|
|
46
50
|
log_debug "[ApiKeys Auth] authenticate_api_key! started for request: #{request.uuid}"
|
|
47
51
|
|
|
48
52
|
# Enqueue before_authentication callback asynchronously
|
|
@@ -50,13 +54,12 @@ module ApiKeys
|
|
|
50
54
|
|
|
51
55
|
# Perform synchronous authentication
|
|
52
56
|
result = Services::Authenticator.call(request)
|
|
53
|
-
log_debug "[ApiKeys Auth]
|
|
57
|
+
log_debug "[ApiKeys Auth] Authentication result: success=#{result.success?}, error_code=#{result.error_code || 'none'}"
|
|
54
58
|
|
|
55
59
|
# Prepare context for after_authentication callback
|
|
56
60
|
after_auth_context = {
|
|
57
61
|
success: result.success?,
|
|
58
62
|
error_code: result.error_code,
|
|
59
|
-
message: result.message,
|
|
60
63
|
api_key_id: result.api_key&.id # Pass ID only, not the full object
|
|
61
64
|
}
|
|
62
65
|
|
|
@@ -65,10 +68,18 @@ module ApiKeys
|
|
|
65
68
|
log_debug "[ApiKeys Auth] Authentication successful. Key ID: #{@current_api_key.id}"
|
|
66
69
|
|
|
67
70
|
if scope && !check_api_key_scopes(scope)
|
|
68
|
-
log_debug "[ApiKeys Auth] Scope check failed
|
|
71
|
+
log_debug "[ApiKeys Auth] Scope check failed for key ID #{@current_api_key.id}."
|
|
69
72
|
# Add required scope info to context before rendering/enqueueing
|
|
70
73
|
after_auth_context[:required_scope_check] = { required: scope, passed: false }
|
|
71
|
-
|
|
74
|
+
after_auth_context[:success] = false
|
|
75
|
+
after_auth_context[:error_code] = :missing_scope
|
|
76
|
+
@current_api_key = nil
|
|
77
|
+
render_unauthorized(
|
|
78
|
+
error_code: :missing_scope,
|
|
79
|
+
message: "API key does not have the required scope(s): #{scope}",
|
|
80
|
+
status: :forbidden,
|
|
81
|
+
required_scope: scope
|
|
82
|
+
)
|
|
72
83
|
else
|
|
73
84
|
after_auth_context[:required_scope_check] = { required: scope, passed: true } if scope
|
|
74
85
|
# Authentication and scope check successful, enqueue stats update
|
|
@@ -90,7 +101,7 @@ module ApiKeys
|
|
|
90
101
|
# @param required_scopes [String, Array<String>] The required scope(s).
|
|
91
102
|
# @return [Boolean] True if the key has all required scopes, false otherwise.
|
|
92
103
|
def check_api_key_scopes(required_scopes)
|
|
93
|
-
return
|
|
104
|
+
return false unless current_api_key
|
|
94
105
|
return true if required_scopes.blank?
|
|
95
106
|
|
|
96
107
|
Array(required_scopes).all? do |req_scope|
|
|
@@ -112,6 +123,11 @@ module ApiKeys
|
|
|
112
123
|
return unless ApiKeys.configuration.enable_async_operations
|
|
113
124
|
return unless current_api_key
|
|
114
125
|
|
|
126
|
+
if stats_update_debounced?
|
|
127
|
+
log_debug "[ApiKeys Auth] Skipping a recently recorded last-used update for ApiKey ID: #{current_api_key.id}"
|
|
128
|
+
return
|
|
129
|
+
end
|
|
130
|
+
|
|
115
131
|
# Check ActiveJob configuration and warn if using suboptimal adapters
|
|
116
132
|
adapter = ActiveJob::Base.queue_adapter
|
|
117
133
|
if adapter.is_a?(ActiveJob::QueueAdapters::InlineAdapter)
|
|
@@ -124,11 +140,23 @@ module ApiKeys
|
|
|
124
140
|
timestamp = Time.current # Capture time once for the job
|
|
125
141
|
log_debug "[ApiKeys Auth] Enqueuing UpdateStatsJob for ApiKey ID: #{current_api_key.id} at #{timestamp}"
|
|
126
142
|
ApiKeys::Jobs::UpdateStatsJob.perform_later(current_api_key.id, timestamp)
|
|
127
|
-
rescue StandardError =>
|
|
128
|
-
log_error "[ApiKeys Auth] Failed to enqueue UpdateStatsJob for key #{current_api_key.id}
|
|
143
|
+
rescue StandardError => error
|
|
144
|
+
log_error "[ApiKeys Auth] Failed to enqueue UpdateStatsJob for key #{current_api_key.id} (#{error.class})."
|
|
129
145
|
end
|
|
130
146
|
end
|
|
131
147
|
|
|
148
|
+
def stats_update_debounced?
|
|
149
|
+
config = ApiKeys.configuration
|
|
150
|
+
return false if config.track_requests_count
|
|
151
|
+
|
|
152
|
+
interval = config.stats_update_interval
|
|
153
|
+
seconds = interval.to_f if interval.respond_to?(:to_f)
|
|
154
|
+
return false unless seconds&.finite? && seconds.positive?
|
|
155
|
+
return false unless current_api_key.last_used_at
|
|
156
|
+
|
|
157
|
+
current_api_key.last_used_at >= Time.current - seconds
|
|
158
|
+
end
|
|
159
|
+
|
|
132
160
|
# Helper to safely enqueue callback jobs.
|
|
133
161
|
def enqueue_callback(callback_type, context)
|
|
134
162
|
# Return early if async operations are globally disabled
|
|
@@ -148,10 +176,10 @@ module ApiKeys
|
|
|
148
176
|
|
|
149
177
|
# Proceed with enqueueing if it's a configured callback
|
|
150
178
|
begin
|
|
151
|
-
log_debug "[ApiKeys Auth] Enqueuing
|
|
179
|
+
log_debug "[ApiKeys Auth] Enqueuing callback job for type: #{callback_type}"
|
|
152
180
|
ApiKeys::Jobs::CallbacksJob.perform_later(callback_type, context)
|
|
153
|
-
rescue StandardError =>
|
|
154
|
-
log_error "[ApiKeys Auth] Failed to enqueue CallbacksJob for type #{callback_type}
|
|
181
|
+
rescue StandardError => error
|
|
182
|
+
log_error "[ApiKeys Auth] Failed to enqueue CallbacksJob for type #{callback_type} (#{error.class})."
|
|
155
183
|
# Don't fail the request if callback enqueueing fails
|
|
156
184
|
end
|
|
157
185
|
end
|