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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -0
  3. data/README.md +24 -0
  4. data/lib/better_auth/adapters/internal_adapter.rb +5 -5
  5. data/lib/better_auth/adapters/sql.rb +96 -18
  6. data/lib/better_auth/api.rb +113 -13
  7. data/lib/better_auth/configuration.rb +97 -7
  8. data/lib/better_auth/context.rb +165 -12
  9. data/lib/better_auth/cookies.rb +6 -4
  10. data/lib/better_auth/core.rb +2 -0
  11. data/lib/better_auth/crypto/jwe.rb +27 -5
  12. data/lib/better_auth/crypto.rb +32 -0
  13. data/lib/better_auth/database_hooks.rb +5 -5
  14. data/lib/better_auth/endpoint.rb +87 -3
  15. data/lib/better_auth/error.rb +8 -1
  16. data/lib/better_auth/plugins/admin/schema.rb +2 -2
  17. data/lib/better_auth/plugins/admin.rb +344 -16
  18. data/lib/better_auth/plugins/anonymous.rb +37 -3
  19. data/lib/better_auth/plugins/device_authorization.rb +102 -5
  20. data/lib/better_auth/plugins/dub.rb +148 -0
  21. data/lib/better_auth/plugins/email_otp.rb +246 -15
  22. data/lib/better_auth/plugins/expo.rb +17 -1
  23. data/lib/better_auth/plugins/generic_oauth.rb +53 -7
  24. data/lib/better_auth/plugins/jwt.rb +37 -4
  25. data/lib/better_auth/plugins/last_login_method.rb +2 -2
  26. data/lib/better_auth/plugins/magic_link.rb +66 -3
  27. data/lib/better_auth/plugins/mcp/authorization.rb +111 -0
  28. data/lib/better_auth/plugins/mcp/config.rb +51 -0
  29. data/lib/better_auth/plugins/mcp/consent.rb +31 -0
  30. data/lib/better_auth/plugins/mcp/legacy_aliases.rb +39 -0
  31. data/lib/better_auth/plugins/mcp/metadata.rb +81 -0
  32. data/lib/better_auth/plugins/mcp/registration.rb +31 -0
  33. data/lib/better_auth/plugins/mcp/resource_handler.rb +37 -0
  34. data/lib/better_auth/plugins/mcp/schema.rb +91 -0
  35. data/lib/better_auth/plugins/mcp/token.rb +108 -0
  36. data/lib/better_auth/plugins/mcp/userinfo.rb +37 -0
  37. data/lib/better_auth/plugins/mcp.rb +111 -263
  38. data/lib/better_auth/plugins/multi_session.rb +61 -3
  39. data/lib/better_auth/plugins/oauth_protocol.rb +2 -2
  40. data/lib/better_auth/plugins/oauth_proxy.rb +26 -6
  41. data/lib/better_auth/plugins/oidc_provider.rb +118 -14
  42. data/lib/better_auth/plugins/one_tap.rb +7 -2
  43. data/lib/better_auth/plugins/one_time_token.rb +42 -2
  44. data/lib/better_auth/plugins/open_api.rb +163 -318
  45. data/lib/better_auth/plugins/organization.rb +135 -36
  46. data/lib/better_auth/plugins/phone_number.rb +141 -6
  47. data/lib/better_auth/plugins/siwe.rb +69 -3
  48. data/lib/better_auth/plugins/two_factor.rb +65 -23
  49. data/lib/better_auth/plugins/username.rb +57 -2
  50. data/lib/better_auth/rate_limiter.rb +20 -0
  51. data/lib/better_auth/response.rb +42 -0
  52. data/lib/better_auth/router.rb +7 -1
  53. data/lib/better_auth/routes/account.rb +204 -38
  54. data/lib/better_auth/routes/email_verification.rb +98 -14
  55. data/lib/better_auth/routes/password.rb +125 -8
  56. data/lib/better_auth/routes/session.rb +128 -13
  57. data/lib/better_auth/routes/sign_in.rb +24 -2
  58. data/lib/better_auth/routes/sign_out.rb +13 -1
  59. data/lib/better_auth/routes/sign_up.rb +62 -4
  60. data/lib/better_auth/routes/social.rb +102 -7
  61. data/lib/better_auth/routes/user.rb +222 -20
  62. data/lib/better_auth/routes/validation.rb +50 -0
  63. data/lib/better_auth/secret_config.rb +115 -0
  64. data/lib/better_auth/session.rb +1 -1
  65. data/lib/better_auth/url_helpers.rb +12 -1
  66. data/lib/better_auth/version.rb +1 -1
  67. data/lib/better_auth.rb +4 -0
  68. 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(path: "/admin/set-role", method: "POST") do |ctx|
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(path: "/admin/get-user", method: "GET") do |ctx|
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(path: "/admin/create-user", method: "POST") do |ctx|
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(path: "/admin/update-user", method: "POST") do |ctx|
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(path: "/admin/list-users", method: "GET") do |ctx|
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(path: "/admin/list-user-sessions", method: "POST") do |ctx|
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(path: "/admin/ban-user", method: "POST") do |ctx|
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(path: "/admin/unban-user", method: "POST") do |ctx|
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(path: "/admin/impersonate-user", method: "POST") do |ctx|
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(path: "/admin/stop-impersonating", method: "POST") do |ctx|
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(path: "/admin/revoke-user-session", method: "POST") do |ctx|
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(path: "/admin/revoke-user-sessions", method: "POST") do |ctx|
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(path: "/admin/remove-user", method: "POST") do |ctx|
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(path: "/admin/set-user-password", method: "POST") do |ctx|
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(path: "/admin/has-permission", method: "POST") do |ctx|
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(path: "/sign-in/anonymous", method: "POST") do |ctx|
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(path: "/delete-anonymous-user", method: "POST") do |ctx|
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(path: "/device/code", method: "POST") do |ctx|
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(path: "/device/token", method: "POST", metadata: {allowed_media_types: ["application/x-www-form-urlencoded", "application/json"]}) do |ctx|
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(path: "/device", method: "GET") do |ctx|
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(path: "/device/approve", method: "POST") do |ctx|
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(path: "/device/deny", method: "POST") do |ctx|
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