better_auth 0.4.0 → 0.5.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 +2 -0
- data/README.md +24 -0
- data/lib/better_auth/adapters/internal_adapter.rb +5 -5
- data/lib/better_auth/adapters/sql.rb +96 -18
- data/lib/better_auth/api.rb +113 -13
- data/lib/better_auth/configuration.rb +97 -7
- data/lib/better_auth/context.rb +165 -12
- data/lib/better_auth/cookies.rb +6 -4
- data/lib/better_auth/core.rb +2 -0
- data/lib/better_auth/crypto/jwe.rb +27 -5
- data/lib/better_auth/crypto.rb +32 -0
- data/lib/better_auth/database_hooks.rb +5 -5
- data/lib/better_auth/endpoint.rb +87 -3
- data/lib/better_auth/error.rb +8 -1
- data/lib/better_auth/plugins/admin/schema.rb +2 -2
- data/lib/better_auth/plugins/admin.rb +344 -16
- data/lib/better_auth/plugins/anonymous.rb +37 -3
- data/lib/better_auth/plugins/device_authorization.rb +102 -5
- data/lib/better_auth/plugins/dub.rb +148 -0
- data/lib/better_auth/plugins/email_otp.rb +246 -15
- data/lib/better_auth/plugins/expo.rb +17 -1
- data/lib/better_auth/plugins/generic_oauth.rb +53 -7
- data/lib/better_auth/plugins/jwt.rb +37 -4
- data/lib/better_auth/plugins/last_login_method.rb +2 -2
- data/lib/better_auth/plugins/magic_link.rb +66 -3
- data/lib/better_auth/plugins/mcp/authorization.rb +111 -0
- data/lib/better_auth/plugins/mcp/config.rb +51 -0
- data/lib/better_auth/plugins/mcp/consent.rb +31 -0
- data/lib/better_auth/plugins/mcp/legacy_aliases.rb +39 -0
- data/lib/better_auth/plugins/mcp/metadata.rb +81 -0
- data/lib/better_auth/plugins/mcp/registration.rb +31 -0
- data/lib/better_auth/plugins/mcp/resource_handler.rb +37 -0
- data/lib/better_auth/plugins/mcp/schema.rb +91 -0
- data/lib/better_auth/plugins/mcp/token.rb +108 -0
- data/lib/better_auth/plugins/mcp/userinfo.rb +37 -0
- data/lib/better_auth/plugins/mcp.rb +111 -263
- data/lib/better_auth/plugins/multi_session.rb +61 -3
- data/lib/better_auth/plugins/oauth_protocol.rb +2 -2
- data/lib/better_auth/plugins/oauth_proxy.rb +26 -6
- data/lib/better_auth/plugins/oidc_provider.rb +118 -14
- data/lib/better_auth/plugins/one_tap.rb +7 -2
- data/lib/better_auth/plugins/one_time_token.rb +42 -2
- data/lib/better_auth/plugins/open_api.rb +163 -318
- data/lib/better_auth/plugins/organization.rb +135 -36
- data/lib/better_auth/plugins/phone_number.rb +141 -6
- data/lib/better_auth/plugins/siwe.rb +69 -3
- data/lib/better_auth/plugins/two_factor.rb +65 -23
- data/lib/better_auth/plugins/username.rb +57 -2
- data/lib/better_auth/rate_limiter.rb +20 -0
- data/lib/better_auth/response.rb +42 -0
- data/lib/better_auth/router.rb +7 -1
- data/lib/better_auth/routes/account.rb +204 -38
- data/lib/better_auth/routes/email_verification.rb +98 -14
- data/lib/better_auth/routes/password.rb +125 -8
- data/lib/better_auth/routes/session.rb +128 -13
- data/lib/better_auth/routes/sign_in.rb +24 -2
- data/lib/better_auth/routes/sign_out.rb +13 -1
- data/lib/better_auth/routes/sign_up.rb +62 -4
- data/lib/better_auth/routes/social.rb +102 -7
- data/lib/better_auth/routes/user.rb +222 -20
- data/lib/better_auth/routes/validation.rb +50 -0
- data/lib/better_auth/secret_config.rb +115 -0
- data/lib/better_auth/session.rb +1 -1
- data/lib/better_auth/url_helpers.rb +12 -1
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +4 -0
- metadata +15 -1
|
@@ -135,7 +135,20 @@ module BetterAuth
|
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
def admin_set_role_endpoint(config)
|
|
138
|
-
Endpoint.new(
|
|
138
|
+
Endpoint.new(
|
|
139
|
+
path: "/admin/set-role",
|
|
140
|
+
method: "POST",
|
|
141
|
+
metadata: admin_user_mutation_openapi(
|
|
142
|
+
operation_id: "setUserRole",
|
|
143
|
+
description: "Set the role of a user",
|
|
144
|
+
response_description: "User role updated",
|
|
145
|
+
properties: {
|
|
146
|
+
userId: {type: "string", description: "The user id"},
|
|
147
|
+
role: {type: ["string", "array"], description: "The role or roles to set"}
|
|
148
|
+
},
|
|
149
|
+
required: ["userId", "role"]
|
|
150
|
+
)
|
|
151
|
+
) do |ctx|
|
|
139
152
|
admin_require_permission!(ctx, config, {user: ["set-role"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE"))
|
|
140
153
|
body = normalize_hash(ctx.body)
|
|
141
154
|
user_id = body[:user_id].to_s
|
|
@@ -147,7 +160,24 @@ module BetterAuth
|
|
|
147
160
|
end
|
|
148
161
|
|
|
149
162
|
def admin_get_user_endpoint(config)
|
|
150
|
-
Endpoint.new(
|
|
163
|
+
Endpoint.new(
|
|
164
|
+
path: "/admin/get-user",
|
|
165
|
+
method: "GET",
|
|
166
|
+
metadata: {
|
|
167
|
+
openapi: {
|
|
168
|
+
operationId: "getUser",
|
|
169
|
+
description: "Get an existing user",
|
|
170
|
+
parameters: [
|
|
171
|
+
{name: "id", in: "query", required: false, schema: {type: "string"}},
|
|
172
|
+
{name: "userId", in: "query", required: false, schema: {type: "string"}},
|
|
173
|
+
{name: "email", in: "query", required: false, schema: {type: "string"}}
|
|
174
|
+
],
|
|
175
|
+
responses: {
|
|
176
|
+
"200" => OpenAPI.json_response("User", {type: "object", "$ref": "#/components/schemas/User"})
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
) do |ctx|
|
|
151
181
|
admin_require_permission!(ctx, config, {user: ["get"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_GET_USER"))
|
|
152
182
|
query = normalize_hash(ctx.query)
|
|
153
183
|
user = if query[:id] || query[:user_id]
|
|
@@ -161,7 +191,23 @@ module BetterAuth
|
|
|
161
191
|
end
|
|
162
192
|
|
|
163
193
|
def admin_create_user_endpoint(config)
|
|
164
|
-
Endpoint.new(
|
|
194
|
+
Endpoint.new(
|
|
195
|
+
path: "/admin/create-user",
|
|
196
|
+
method: "POST",
|
|
197
|
+
metadata: admin_user_mutation_openapi(
|
|
198
|
+
operation_id: "createUser",
|
|
199
|
+
description: "Create a new user",
|
|
200
|
+
response_description: "User created",
|
|
201
|
+
properties: {
|
|
202
|
+
email: {type: "string", description: "The email of the user"},
|
|
203
|
+
password: {type: ["string", "null"], description: "The password of the user"},
|
|
204
|
+
name: {type: "string", description: "The name of the user"},
|
|
205
|
+
role: {type: ["string", "array", "null"], description: "The role or roles of the user"},
|
|
206
|
+
data: {type: ["object", "null"], description: "Additional user data"}
|
|
207
|
+
},
|
|
208
|
+
required: ["email", "name"]
|
|
209
|
+
)
|
|
210
|
+
) do |ctx|
|
|
165
211
|
session = Routes.current_session(ctx, allow_nil: true)
|
|
166
212
|
if session
|
|
167
213
|
unless admin_permission?(session[:user], session[:user]["role"], {user: ["create"]}, config)
|
|
@@ -183,7 +229,7 @@ module BetterAuth
|
|
|
183
229
|
name: body[:name].to_s,
|
|
184
230
|
email: email,
|
|
185
231
|
role: admin_validate_roles!(body[:role] || config[:default_role], config)
|
|
186
|
-
).merge(body.key?(:image) ? {image: body[:image]} : {}))
|
|
232
|
+
).merge(body.key?(:image) ? {image: body[:image]} : {}), context: ctx)
|
|
187
233
|
raise APIError.new("INTERNAL_SERVER_ERROR", message: ADMIN_ERROR_CODES.fetch("FAILED_TO_CREATE_USER")) unless user
|
|
188
234
|
|
|
189
235
|
if body[:password].to_s != ""
|
|
@@ -194,7 +240,28 @@ module BetterAuth
|
|
|
194
240
|
end
|
|
195
241
|
|
|
196
242
|
def admin_update_user_endpoint(config)
|
|
197
|
-
Endpoint.new(
|
|
243
|
+
Endpoint.new(
|
|
244
|
+
path: "/admin/update-user",
|
|
245
|
+
method: "POST",
|
|
246
|
+
metadata: {
|
|
247
|
+
openapi: {
|
|
248
|
+
operationId: "adminUpdateUser",
|
|
249
|
+
description: "Update a user's details",
|
|
250
|
+
requestBody: OpenAPI.json_request_body(
|
|
251
|
+
OpenAPI.object_schema(
|
|
252
|
+
{
|
|
253
|
+
userId: {type: "string", description: "The user id"},
|
|
254
|
+
data: {type: "object", description: "The user data to update"}
|
|
255
|
+
},
|
|
256
|
+
required: ["userId", "data"]
|
|
257
|
+
)
|
|
258
|
+
),
|
|
259
|
+
responses: {
|
|
260
|
+
"200" => OpenAPI.json_response("User updated", {type: "object", "$ref": "#/components/schemas/User"})
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
) do |ctx|
|
|
198
265
|
admin_require_permission!(ctx, config, {user: ["update"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS"))
|
|
199
266
|
body = normalize_hash(ctx.body)
|
|
200
267
|
data = normalize_hash(body[:data] || body).except(:user_id, :data)
|
|
@@ -209,7 +276,31 @@ module BetterAuth
|
|
|
209
276
|
end
|
|
210
277
|
|
|
211
278
|
def admin_list_users_endpoint(config)
|
|
212
|
-
Endpoint.new(
|
|
279
|
+
Endpoint.new(
|
|
280
|
+
path: "/admin/list-users",
|
|
281
|
+
method: "GET",
|
|
282
|
+
metadata: {
|
|
283
|
+
openapi: {
|
|
284
|
+
operationId: "listUsers",
|
|
285
|
+
description: "List users",
|
|
286
|
+
parameters: admin_list_users_parameters,
|
|
287
|
+
responses: {
|
|
288
|
+
"200" => OpenAPI.json_response(
|
|
289
|
+
"List of users",
|
|
290
|
+
OpenAPI.object_schema(
|
|
291
|
+
{
|
|
292
|
+
users: {type: "array", items: {type: "object", "$ref": "#/components/schemas/User"}},
|
|
293
|
+
total: {type: "number"},
|
|
294
|
+
limit: {type: ["number", "null"]},
|
|
295
|
+
offset: {type: ["number", "null"]}
|
|
296
|
+
},
|
|
297
|
+
required: ["users", "total"]
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
) do |ctx|
|
|
213
304
|
admin_require_permission!(ctx, config, {user: ["list"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_LIST_USERS"))
|
|
214
305
|
query = normalize_hash(ctx.query)
|
|
215
306
|
where = admin_user_where(query)
|
|
@@ -228,7 +319,15 @@ module BetterAuth
|
|
|
228
319
|
end
|
|
229
320
|
|
|
230
321
|
def admin_list_user_sessions_endpoint(config)
|
|
231
|
-
Endpoint.new(
|
|
322
|
+
Endpoint.new(
|
|
323
|
+
path: "/admin/list-user-sessions",
|
|
324
|
+
method: "POST",
|
|
325
|
+
metadata: admin_sessions_openapi(
|
|
326
|
+
operation_id: "adminListUserSessions",
|
|
327
|
+
description: "List user sessions",
|
|
328
|
+
response_description: "List of user sessions"
|
|
329
|
+
)
|
|
330
|
+
) do |ctx|
|
|
232
331
|
admin_require_permission!(ctx, config, {session: ["list"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS"))
|
|
233
332
|
sessions = ctx.context.internal_adapter.list_sessions(normalize_hash(ctx.body)[:user_id])
|
|
234
333
|
ctx.json({sessions: sessions.map { |session| Schema.parse_output(ctx.context.options, "session", session) }})
|
|
@@ -236,7 +335,21 @@ module BetterAuth
|
|
|
236
335
|
end
|
|
237
336
|
|
|
238
337
|
def admin_ban_user_endpoint(config)
|
|
239
|
-
Endpoint.new(
|
|
338
|
+
Endpoint.new(
|
|
339
|
+
path: "/admin/ban-user",
|
|
340
|
+
method: "POST",
|
|
341
|
+
metadata: admin_user_mutation_openapi(
|
|
342
|
+
operation_id: "banUser",
|
|
343
|
+
description: "Ban a user",
|
|
344
|
+
response_description: "User banned",
|
|
345
|
+
properties: {
|
|
346
|
+
userId: {type: "string", description: "The user id"},
|
|
347
|
+
banReason: {type: ["string", "null"], description: "The reason for the ban"},
|
|
348
|
+
banExpiresIn: {type: ["number", "null"], description: "The number of seconds until the ban expires"}
|
|
349
|
+
},
|
|
350
|
+
required: ["userId"]
|
|
351
|
+
)
|
|
352
|
+
) do |ctx|
|
|
240
353
|
session = admin_require_permission!(ctx, config, {user: ["ban"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_BAN_USERS"))
|
|
241
354
|
body = normalize_hash(ctx.body)
|
|
242
355
|
found = ctx.context.internal_adapter.find_user_by_id(body[:user_id])
|
|
@@ -250,7 +363,19 @@ module BetterAuth
|
|
|
250
363
|
end
|
|
251
364
|
|
|
252
365
|
def admin_unban_user_endpoint(config)
|
|
253
|
-
Endpoint.new(
|
|
366
|
+
Endpoint.new(
|
|
367
|
+
path: "/admin/unban-user",
|
|
368
|
+
method: "POST",
|
|
369
|
+
metadata: admin_user_mutation_openapi(
|
|
370
|
+
operation_id: "unbanUser",
|
|
371
|
+
description: "Unban a user",
|
|
372
|
+
response_description: "User unbanned",
|
|
373
|
+
properties: {
|
|
374
|
+
userId: {type: "string", description: "The user id"}
|
|
375
|
+
},
|
|
376
|
+
required: ["userId"]
|
|
377
|
+
)
|
|
378
|
+
) do |ctx|
|
|
254
379
|
admin_require_permission!(ctx, config, {user: ["ban"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_BAN_USERS"))
|
|
255
380
|
user = ctx.context.internal_adapter.update_user(normalize_hash(ctx.body)[:user_id], banned: false, banReason: nil, banExpires: nil, updatedAt: Time.now)
|
|
256
381
|
ctx.json({user: Schema.parse_output(ctx.context.options, "user", user)})
|
|
@@ -258,7 +383,27 @@ module BetterAuth
|
|
|
258
383
|
end
|
|
259
384
|
|
|
260
385
|
def admin_impersonate_user_endpoint(config)
|
|
261
|
-
Endpoint.new(
|
|
386
|
+
Endpoint.new(
|
|
387
|
+
path: "/admin/impersonate-user",
|
|
388
|
+
method: "POST",
|
|
389
|
+
metadata: {
|
|
390
|
+
openapi: {
|
|
391
|
+
operationId: "impersonateUser",
|
|
392
|
+
description: "Impersonate a user",
|
|
393
|
+
requestBody: OpenAPI.json_request_body(
|
|
394
|
+
OpenAPI.object_schema(
|
|
395
|
+
{
|
|
396
|
+
userId: {type: "string", description: "The user id"}
|
|
397
|
+
},
|
|
398
|
+
required: ["userId"]
|
|
399
|
+
)
|
|
400
|
+
),
|
|
401
|
+
responses: {
|
|
402
|
+
"200" => OpenAPI.json_response("Impersonation session created", OpenAPI.session_response_schema_pair)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
) do |ctx|
|
|
262
407
|
session = admin_require_permission!(ctx, config, {user: ["impersonate"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS"))
|
|
263
408
|
body = normalize_hash(ctx.body)
|
|
264
409
|
target = ctx.context.internal_adapter.find_user_by_id(body[:user_id])
|
|
@@ -284,7 +429,19 @@ module BetterAuth
|
|
|
284
429
|
end
|
|
285
430
|
|
|
286
431
|
def admin_stop_impersonating_endpoint
|
|
287
|
-
Endpoint.new(
|
|
432
|
+
Endpoint.new(
|
|
433
|
+
path: "/admin/stop-impersonating",
|
|
434
|
+
method: "POST",
|
|
435
|
+
metadata: {
|
|
436
|
+
openapi: {
|
|
437
|
+
operationId: "stopImpersonating",
|
|
438
|
+
description: "Stop impersonating a user",
|
|
439
|
+
responses: {
|
|
440
|
+
"200" => OpenAPI.json_response("Impersonation stopped", OpenAPI.session_response_schema_pair)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
) do |ctx|
|
|
288
445
|
session = Routes.current_session(ctx, sensitive: true)
|
|
289
446
|
admin_id = session[:session]["impersonatedBy"]
|
|
290
447
|
raise APIError.new("BAD_REQUEST", message: "You are not impersonating anyone") unless admin_id
|
|
@@ -312,7 +469,27 @@ module BetterAuth
|
|
|
312
469
|
end
|
|
313
470
|
|
|
314
471
|
def admin_revoke_user_session_endpoint(config)
|
|
315
|
-
Endpoint.new(
|
|
472
|
+
Endpoint.new(
|
|
473
|
+
path: "/admin/revoke-user-session",
|
|
474
|
+
method: "POST",
|
|
475
|
+
metadata: {
|
|
476
|
+
openapi: {
|
|
477
|
+
operationId: "revokeUserSession",
|
|
478
|
+
description: "Revoke a user session",
|
|
479
|
+
requestBody: OpenAPI.json_request_body(
|
|
480
|
+
OpenAPI.object_schema(
|
|
481
|
+
{
|
|
482
|
+
sessionToken: {type: "string", description: "The session token"}
|
|
483
|
+
},
|
|
484
|
+
required: ["sessionToken"]
|
|
485
|
+
)
|
|
486
|
+
),
|
|
487
|
+
responses: {
|
|
488
|
+
"200" => OpenAPI.json_response("Session revoked", OpenAPI.success_response_schema)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
) do |ctx|
|
|
316
493
|
admin_require_permission!(ctx, config, {session: ["revoke"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS"))
|
|
317
494
|
ctx.context.internal_adapter.delete_session(normalize_hash(ctx.body)[:session_token])
|
|
318
495
|
ctx.json({success: true})
|
|
@@ -320,7 +497,27 @@ module BetterAuth
|
|
|
320
497
|
end
|
|
321
498
|
|
|
322
499
|
def admin_revoke_user_sessions_endpoint(config)
|
|
323
|
-
Endpoint.new(
|
|
500
|
+
Endpoint.new(
|
|
501
|
+
path: "/admin/revoke-user-sessions",
|
|
502
|
+
method: "POST",
|
|
503
|
+
metadata: {
|
|
504
|
+
openapi: {
|
|
505
|
+
operationId: "revokeUserSessions",
|
|
506
|
+
description: "Revoke all user sessions",
|
|
507
|
+
requestBody: OpenAPI.json_request_body(
|
|
508
|
+
OpenAPI.object_schema(
|
|
509
|
+
{
|
|
510
|
+
userId: {type: "string", description: "The user id"}
|
|
511
|
+
},
|
|
512
|
+
required: ["userId"]
|
|
513
|
+
)
|
|
514
|
+
),
|
|
515
|
+
responses: {
|
|
516
|
+
"200" => OpenAPI.json_response("Sessions revoked", OpenAPI.success_response_schema)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
) do |ctx|
|
|
324
521
|
admin_require_permission!(ctx, config, {session: ["revoke"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS"))
|
|
325
522
|
ctx.context.internal_adapter.delete_sessions(normalize_hash(ctx.body)[:user_id])
|
|
326
523
|
ctx.json({success: true})
|
|
@@ -328,7 +525,27 @@ module BetterAuth
|
|
|
328
525
|
end
|
|
329
526
|
|
|
330
527
|
def admin_remove_user_endpoint(config)
|
|
331
|
-
Endpoint.new(
|
|
528
|
+
Endpoint.new(
|
|
529
|
+
path: "/admin/remove-user",
|
|
530
|
+
method: "POST",
|
|
531
|
+
metadata: {
|
|
532
|
+
openapi: {
|
|
533
|
+
operationId: "removeUser",
|
|
534
|
+
description: "Remove a user",
|
|
535
|
+
requestBody: OpenAPI.json_request_body(
|
|
536
|
+
OpenAPI.object_schema(
|
|
537
|
+
{
|
|
538
|
+
userId: {type: "string", description: "The user id"}
|
|
539
|
+
},
|
|
540
|
+
required: ["userId"]
|
|
541
|
+
)
|
|
542
|
+
),
|
|
543
|
+
responses: {
|
|
544
|
+
"200" => OpenAPI.json_response("User removed", OpenAPI.success_response_schema)
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
) do |ctx|
|
|
332
549
|
session = admin_require_permission!(ctx, config, {user: ["delete"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS"))
|
|
333
550
|
user_id = normalize_hash(ctx.body)[:user_id]
|
|
334
551
|
raise APIError.new("BAD_REQUEST", message: ADMIN_ERROR_CODES.fetch("YOU_CANNOT_REMOVE_YOURSELF")) if user_id == session[:user]["id"]
|
|
@@ -339,7 +556,28 @@ module BetterAuth
|
|
|
339
556
|
end
|
|
340
557
|
|
|
341
558
|
def admin_set_user_password_endpoint(config)
|
|
342
|
-
Endpoint.new(
|
|
559
|
+
Endpoint.new(
|
|
560
|
+
path: "/admin/set-user-password",
|
|
561
|
+
method: "POST",
|
|
562
|
+
metadata: {
|
|
563
|
+
openapi: {
|
|
564
|
+
operationId: "setUserPassword",
|
|
565
|
+
description: "Set a user's password",
|
|
566
|
+
requestBody: OpenAPI.json_request_body(
|
|
567
|
+
OpenAPI.object_schema(
|
|
568
|
+
{
|
|
569
|
+
userId: {type: "string", description: "The user id"},
|
|
570
|
+
newPassword: {type: "string", description: "The new password"}
|
|
571
|
+
},
|
|
572
|
+
required: ["userId", "newPassword"]
|
|
573
|
+
)
|
|
574
|
+
),
|
|
575
|
+
responses: {
|
|
576
|
+
"200" => OpenAPI.json_response("Password set", OpenAPI.status_response_schema)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
) do |ctx|
|
|
343
581
|
admin_require_permission!(ctx, config, {user: ["set-password"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD"))
|
|
344
582
|
body = normalize_hash(ctx.body)
|
|
345
583
|
user_id = body[:user_id].to_s
|
|
@@ -355,7 +593,39 @@ module BetterAuth
|
|
|
355
593
|
end
|
|
356
594
|
|
|
357
595
|
def admin_has_permission_endpoint(config)
|
|
358
|
-
Endpoint.new(
|
|
596
|
+
Endpoint.new(
|
|
597
|
+
path: "/admin/has-permission",
|
|
598
|
+
method: "POST",
|
|
599
|
+
body_schema: ->(body) { body.is_a?(Hash) ? body : false },
|
|
600
|
+
metadata: {
|
|
601
|
+
openapi: {
|
|
602
|
+
operationId: "hasPermission",
|
|
603
|
+
description: "Check if the user has permission",
|
|
604
|
+
requestBody: OpenAPI.json_request_body(
|
|
605
|
+
OpenAPI.object_schema(
|
|
606
|
+
{
|
|
607
|
+
permissions: {type: "object", description: "The permissions to check"},
|
|
608
|
+
userId: {type: ["string", "null"], description: "The user id"},
|
|
609
|
+
role: {type: ["string", "null"], description: "The role to check"}
|
|
610
|
+
},
|
|
611
|
+
required: ["permissions"]
|
|
612
|
+
)
|
|
613
|
+
),
|
|
614
|
+
responses: {
|
|
615
|
+
"200" => OpenAPI.json_response(
|
|
616
|
+
"Success",
|
|
617
|
+
OpenAPI.object_schema(
|
|
618
|
+
{
|
|
619
|
+
error: {type: ["string", "null"]},
|
|
620
|
+
success: {type: "boolean"}
|
|
621
|
+
},
|
|
622
|
+
required: ["success"]
|
|
623
|
+
)
|
|
624
|
+
)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
) do |ctx|
|
|
359
629
|
session = Routes.current_session(ctx, allow_nil: true)
|
|
360
630
|
body = normalize_hash(ctx.body)
|
|
361
631
|
permissions = body[:permissions] || body[:permission]
|
|
@@ -386,6 +656,64 @@ module BetterAuth
|
|
|
386
656
|
end
|
|
387
657
|
end
|
|
388
658
|
|
|
659
|
+
def admin_user_mutation_openapi(operation_id:, description:, response_description:, properties:, required:)
|
|
660
|
+
{
|
|
661
|
+
openapi: {
|
|
662
|
+
operationId: operation_id,
|
|
663
|
+
description: description,
|
|
664
|
+
requestBody: OpenAPI.json_request_body(OpenAPI.object_schema(properties, required: required)),
|
|
665
|
+
responses: {
|
|
666
|
+
"200" => OpenAPI.json_response(
|
|
667
|
+
response_description,
|
|
668
|
+
OpenAPI.object_schema(
|
|
669
|
+
{user: {type: "object", "$ref": "#/components/schemas/User"}},
|
|
670
|
+
required: ["user"]
|
|
671
|
+
)
|
|
672
|
+
)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def admin_sessions_openapi(operation_id:, description:, response_description:)
|
|
679
|
+
{
|
|
680
|
+
openapi: {
|
|
681
|
+
operationId: operation_id,
|
|
682
|
+
description: description,
|
|
683
|
+
requestBody: OpenAPI.json_request_body(
|
|
684
|
+
OpenAPI.object_schema(
|
|
685
|
+
{userId: {type: "string", description: "The user id"}},
|
|
686
|
+
required: ["userId"]
|
|
687
|
+
)
|
|
688
|
+
),
|
|
689
|
+
responses: {
|
|
690
|
+
"200" => OpenAPI.json_response(
|
|
691
|
+
response_description,
|
|
692
|
+
OpenAPI.object_schema(
|
|
693
|
+
{sessions: {type: "array", items: {type: "object", "$ref": "#/components/schemas/Session"}}},
|
|
694
|
+
required: ["sessions"]
|
|
695
|
+
)
|
|
696
|
+
)
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def admin_list_users_parameters
|
|
703
|
+
[
|
|
704
|
+
{name: "searchValue", in: "query", required: false, schema: {type: "string"}},
|
|
705
|
+
{name: "searchField", in: "query", required: false, schema: {type: "string"}},
|
|
706
|
+
{name: "searchOperator", in: "query", required: false, schema: {type: "string"}},
|
|
707
|
+
{name: "limit", in: "query", required: false, schema: {type: "number"}},
|
|
708
|
+
{name: "offset", in: "query", required: false, schema: {type: "number"}},
|
|
709
|
+
{name: "sortBy", in: "query", required: false, schema: {type: "string"}},
|
|
710
|
+
{name: "sortDirection", in: "query", required: false, schema: {type: "string"}},
|
|
711
|
+
{name: "filterField", in: "query", required: false, schema: {type: "string"}},
|
|
712
|
+
{name: "filterValue", in: "query", required: false, schema: {type: "string"}},
|
|
713
|
+
{name: "filterOperator", in: "query", required: false, schema: {type: "string"}}
|
|
714
|
+
]
|
|
715
|
+
end
|
|
716
|
+
|
|
389
717
|
def admin_require_permission!(ctx, config, permissions, message)
|
|
390
718
|
session = Routes.current_session(ctx, sensitive: true)
|
|
391
719
|
return session if admin_permission?(session[:user], session[:user]["role"], permissions, config)
|
|
@@ -40,7 +40,28 @@ module BetterAuth
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def sign_in_anonymous_endpoint(config)
|
|
43
|
-
Endpoint.new(
|
|
43
|
+
Endpoint.new(
|
|
44
|
+
path: "/sign-in/anonymous",
|
|
45
|
+
method: "POST",
|
|
46
|
+
metadata: {
|
|
47
|
+
openapi: {
|
|
48
|
+
operationId: "signInAnonymous",
|
|
49
|
+
description: "Sign in anonymously",
|
|
50
|
+
responses: {
|
|
51
|
+
"200" => OpenAPI.json_response(
|
|
52
|
+
"Anonymous session created",
|
|
53
|
+
OpenAPI.object_schema(
|
|
54
|
+
{
|
|
55
|
+
token: {type: "string"},
|
|
56
|
+
user: {type: "object", "$ref": "#/components/schemas/User"}
|
|
57
|
+
},
|
|
58
|
+
required: ["token", "user"]
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
) do |ctx|
|
|
44
65
|
existing_session = Session.find_current(ctx, disable_refresh: true)
|
|
45
66
|
if existing_session&.dig(:user, "isAnonymous")
|
|
46
67
|
raise APIError.new("BAD_REQUEST", message: ANONYMOUS_ERROR_CODES["ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY"])
|
|
@@ -54,7 +75,8 @@ module BetterAuth
|
|
|
54
75
|
isAnonymous: true,
|
|
55
76
|
name: name,
|
|
56
77
|
createdAt: Time.now,
|
|
57
|
-
updatedAt: Time.now
|
|
78
|
+
updatedAt: Time.now,
|
|
79
|
+
context: ctx
|
|
58
80
|
)
|
|
59
81
|
raise APIError.new("INTERNAL_SERVER_ERROR", message: ANONYMOUS_ERROR_CODES["FAILED_TO_CREATE_USER"]) unless user
|
|
60
82
|
|
|
@@ -67,7 +89,19 @@ module BetterAuth
|
|
|
67
89
|
end
|
|
68
90
|
|
|
69
91
|
def delete_anonymous_user_endpoint(config)
|
|
70
|
-
Endpoint.new(
|
|
92
|
+
Endpoint.new(
|
|
93
|
+
path: "/delete-anonymous-user",
|
|
94
|
+
method: "POST",
|
|
95
|
+
metadata: {
|
|
96
|
+
openapi: {
|
|
97
|
+
operationId: "deleteAnonymousUser",
|
|
98
|
+
description: "Delete the current anonymous user",
|
|
99
|
+
responses: {
|
|
100
|
+
"200" => OpenAPI.json_response("Anonymous user deleted", OpenAPI.success_response_schema)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
) do |ctx|
|
|
71
105
|
session = Routes.current_session(ctx, sensitive: true)
|
|
72
106
|
|
|
73
107
|
if config[:disable_delete_anonymous_user]
|
|
@@ -47,7 +47,19 @@ module BetterAuth
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def device_code_endpoint(config)
|
|
50
|
-
Endpoint.new(
|
|
50
|
+
Endpoint.new(
|
|
51
|
+
path: "/device/code",
|
|
52
|
+
method: "POST",
|
|
53
|
+
metadata: {
|
|
54
|
+
openapi: {
|
|
55
|
+
operationId: "requestDeviceCode",
|
|
56
|
+
description: "Request a device and user code",
|
|
57
|
+
responses: {
|
|
58
|
+
"200" => OpenAPI.json_response("Success", device_code_response_schema)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
) do |ctx|
|
|
51
63
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
52
64
|
client_id = body["client_id"]
|
|
53
65
|
if config[:validate_client] && !config[:validate_client].call(client_id)
|
|
@@ -86,7 +98,20 @@ module BetterAuth
|
|
|
86
98
|
end
|
|
87
99
|
|
|
88
100
|
def device_token_endpoint(config)
|
|
89
|
-
Endpoint.new(
|
|
101
|
+
Endpoint.new(
|
|
102
|
+
path: "/device/token",
|
|
103
|
+
method: "POST",
|
|
104
|
+
metadata: {
|
|
105
|
+
allowed_media_types: ["application/x-www-form-urlencoded", "application/json"],
|
|
106
|
+
openapi: {
|
|
107
|
+
operationId: "exchangeDeviceToken",
|
|
108
|
+
description: "Exchange device code for access token",
|
|
109
|
+
responses: {
|
|
110
|
+
"200" => OpenAPI.json_response("Success", device_token_response_schema)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
) do |ctx|
|
|
90
115
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
91
116
|
raise device_authorization_error("BAD_REQUEST", "invalid_request", "Unsupported grant type") unless body["grant_type"] == OAuthProtocol::DEVICE_CODE_GRANT
|
|
92
117
|
if config[:validate_client] && !config[:validate_client].call(body["client_id"])
|
|
@@ -141,7 +166,19 @@ module BetterAuth
|
|
|
141
166
|
end
|
|
142
167
|
|
|
143
168
|
def device_verify_endpoint
|
|
144
|
-
Endpoint.new(
|
|
169
|
+
Endpoint.new(
|
|
170
|
+
path: "/device",
|
|
171
|
+
method: "GET",
|
|
172
|
+
metadata: {
|
|
173
|
+
openapi: {
|
|
174
|
+
operationId: "getDeviceVerification",
|
|
175
|
+
description: "Get device verification status",
|
|
176
|
+
responses: {
|
|
177
|
+
"200" => OpenAPI.json_response("Success", device_verification_response_schema)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
) do |ctx|
|
|
145
182
|
code = normalize_user_code(OAuthProtocol.stringify_keys(ctx.query)["user_code"])
|
|
146
183
|
record = find_device_user_code(ctx, code)
|
|
147
184
|
raise device_authorization_error("BAD_REQUEST", "invalid_request", DEVICE_AUTHORIZATION_ERROR_CODES["INVALID_USER_CODE"]) unless record
|
|
@@ -153,7 +190,19 @@ module BetterAuth
|
|
|
153
190
|
end
|
|
154
191
|
|
|
155
192
|
def device_approve_endpoint
|
|
156
|
-
Endpoint.new(
|
|
193
|
+
Endpoint.new(
|
|
194
|
+
path: "/device/approve",
|
|
195
|
+
method: "POST",
|
|
196
|
+
metadata: {
|
|
197
|
+
openapi: {
|
|
198
|
+
operationId: "approveDevice",
|
|
199
|
+
description: "Approve a device authorization request",
|
|
200
|
+
responses: {
|
|
201
|
+
"200" => OpenAPI.json_response("Success", OpenAPI.success_response_schema)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
) do |ctx|
|
|
157
206
|
session = Routes.current_session(ctx, allow_nil: true)
|
|
158
207
|
raise device_authorization_error("UNAUTHORIZED", "unauthorized", DEVICE_AUTHORIZATION_ERROR_CODES["AUTHENTICATION_REQUIRED"]) unless session
|
|
159
208
|
|
|
@@ -162,7 +211,19 @@ module BetterAuth
|
|
|
162
211
|
end
|
|
163
212
|
|
|
164
213
|
def device_deny_endpoint
|
|
165
|
-
Endpoint.new(
|
|
214
|
+
Endpoint.new(
|
|
215
|
+
path: "/device/deny",
|
|
216
|
+
method: "POST",
|
|
217
|
+
metadata: {
|
|
218
|
+
openapi: {
|
|
219
|
+
operationId: "denyDevice",
|
|
220
|
+
description: "Deny a device authorization request",
|
|
221
|
+
responses: {
|
|
222
|
+
"200" => OpenAPI.json_response("Success", OpenAPI.success_response_schema)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
) do |ctx|
|
|
166
227
|
session = Routes.current_session(ctx, allow_nil: true)
|
|
167
228
|
raise device_authorization_error("UNAUTHORIZED", "unauthorized", DEVICE_AUTHORIZATION_ERROR_CODES["AUTHENTICATION_REQUIRED"]) unless session
|
|
168
229
|
|
|
@@ -191,6 +252,42 @@ module BetterAuth
|
|
|
191
252
|
ctx.json({success: true})
|
|
192
253
|
end
|
|
193
254
|
|
|
255
|
+
def device_code_response_schema
|
|
256
|
+
OpenAPI.object_schema(
|
|
257
|
+
{
|
|
258
|
+
device_code: {type: "string", description: "The device verification code"},
|
|
259
|
+
user_code: {type: "string", description: "The user code to display"},
|
|
260
|
+
verification_uri: {type: "string", format: "uri"},
|
|
261
|
+
verification_uri_complete: {type: "string", format: "uri"},
|
|
262
|
+
expires_in: {type: "number"},
|
|
263
|
+
interval: {type: "number"}
|
|
264
|
+
},
|
|
265
|
+
required: ["device_code", "user_code", "verification_uri", "verification_uri_complete", "expires_in", "interval"]
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def device_token_response_schema
|
|
270
|
+
OpenAPI.object_schema(
|
|
271
|
+
{
|
|
272
|
+
access_token: {type: "string"},
|
|
273
|
+
token_type: {type: "string"},
|
|
274
|
+
expires_in: {type: "number"},
|
|
275
|
+
scope: {type: "string"}
|
|
276
|
+
},
|
|
277
|
+
required: ["access_token", "token_type", "expires_in"]
|
|
278
|
+
)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def device_verification_response_schema
|
|
282
|
+
OpenAPI.object_schema(
|
|
283
|
+
{
|
|
284
|
+
user_code: {type: "string"},
|
|
285
|
+
status: {type: "string"}
|
|
286
|
+
},
|
|
287
|
+
required: ["user_code", "status"]
|
|
288
|
+
)
|
|
289
|
+
end
|
|
290
|
+
|
|
194
291
|
def find_device_code(ctx, code)
|
|
195
292
|
ctx.context.adapter.find_one(model: "deviceCode", where: [{field: "deviceCode", value: code.to_s}])
|
|
196
293
|
end
|