better_auth 0.1.1 → 0.2.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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/README.md +106 -16
  4. data/lib/better_auth/adapters/base.rb +49 -0
  5. data/lib/better_auth/adapters/internal_adapter.rb +439 -0
  6. data/lib/better_auth/adapters/memory.rb +232 -0
  7. data/lib/better_auth/adapters/mongodb.rb +369 -0
  8. data/lib/better_auth/adapters/mssql.rb +42 -0
  9. data/lib/better_auth/adapters/mysql.rb +33 -0
  10. data/lib/better_auth/adapters/postgres.rb +17 -0
  11. data/lib/better_auth/adapters/sql.rb +425 -0
  12. data/lib/better_auth/adapters/sqlite.rb +20 -0
  13. data/lib/better_auth/api.rb +226 -0
  14. data/lib/better_auth/api_error.rb +53 -0
  15. data/lib/better_auth/auth.rb +42 -0
  16. data/lib/better_auth/configuration.rb +399 -0
  17. data/lib/better_auth/context.rb +210 -0
  18. data/lib/better_auth/cookies.rb +278 -0
  19. data/lib/better_auth/core.rb +37 -1
  20. data/lib/better_auth/crypto/jwe.rb +76 -0
  21. data/lib/better_auth/crypto.rb +191 -0
  22. data/lib/better_auth/database_hooks.rb +114 -0
  23. data/lib/better_auth/endpoint.rb +326 -0
  24. data/lib/better_auth/error.rb +52 -0
  25. data/lib/better_auth/middleware/origin_check.rb +128 -0
  26. data/lib/better_auth/password.rb +120 -0
  27. data/lib/better_auth/plugin.rb +129 -0
  28. data/lib/better_auth/plugin_context.rb +16 -0
  29. data/lib/better_auth/plugin_registry.rb +67 -0
  30. data/lib/better_auth/plugins/access.rb +87 -0
  31. data/lib/better_auth/plugins/additional_fields.rb +29 -0
  32. data/lib/better_auth/plugins/admin/schema.rb +28 -0
  33. data/lib/better_auth/plugins/admin.rb +518 -0
  34. data/lib/better_auth/plugins/anonymous.rb +198 -0
  35. data/lib/better_auth/plugins/api_key.rb +16 -0
  36. data/lib/better_auth/plugins/bearer.rb +128 -0
  37. data/lib/better_auth/plugins/captcha.rb +159 -0
  38. data/lib/better_auth/plugins/custom_session.rb +84 -0
  39. data/lib/better_auth/plugins/device_authorization.rb +302 -0
  40. data/lib/better_auth/plugins/email_otp.rb +536 -0
  41. data/lib/better_auth/plugins/expo.rb +88 -0
  42. data/lib/better_auth/plugins/generic_oauth.rb +780 -0
  43. data/lib/better_auth/plugins/have_i_been_pwned.rb +94 -0
  44. data/lib/better_auth/plugins/jwt.rb +482 -0
  45. data/lib/better_auth/plugins/last_login_method.rb +92 -0
  46. data/lib/better_auth/plugins/magic_link.rb +181 -0
  47. data/lib/better_auth/plugins/mcp.rb +342 -0
  48. data/lib/better_auth/plugins/multi_session.rb +173 -0
  49. data/lib/better_auth/plugins/oauth_protocol.rb +348 -0
  50. data/lib/better_auth/plugins/oauth_provider.rb +16 -0
  51. data/lib/better_auth/plugins/oauth_proxy.rb +257 -0
  52. data/lib/better_auth/plugins/oidc_provider.rb +597 -0
  53. data/lib/better_auth/plugins/one_tap.rb +154 -0
  54. data/lib/better_auth/plugins/one_time_token.rb +106 -0
  55. data/lib/better_auth/plugins/open_api.rb +489 -0
  56. data/lib/better_auth/plugins/organization/schema.rb +106 -0
  57. data/lib/better_auth/plugins/organization.rb +990 -0
  58. data/lib/better_auth/plugins/passkey.rb +16 -0
  59. data/lib/better_auth/plugins/phone_number.rb +321 -0
  60. data/lib/better_auth/plugins/scim.rb +16 -0
  61. data/lib/better_auth/plugins/siwe.rb +242 -0
  62. data/lib/better_auth/plugins/sso.rb +16 -0
  63. data/lib/better_auth/plugins/stripe.rb +16 -0
  64. data/lib/better_auth/plugins/two_factor.rb +514 -0
  65. data/lib/better_auth/plugins/username.rb +278 -0
  66. data/lib/better_auth/plugins.rb +46 -0
  67. data/lib/better_auth/rate_limiter.rb +215 -0
  68. data/lib/better_auth/request_ip.rb +70 -0
  69. data/lib/better_auth/router.rb +365 -0
  70. data/lib/better_auth/routes/account.rb +211 -0
  71. data/lib/better_auth/routes/email_verification.rb +108 -0
  72. data/lib/better_auth/routes/error.rb +102 -0
  73. data/lib/better_auth/routes/ok.rb +15 -0
  74. data/lib/better_auth/routes/password.rb +164 -0
  75. data/lib/better_auth/routes/session.rb +137 -0
  76. data/lib/better_auth/routes/sign_in.rb +90 -0
  77. data/lib/better_auth/routes/sign_out.rb +15 -0
  78. data/lib/better_auth/routes/sign_up.rb +145 -0
  79. data/lib/better_auth/routes/social.rb +188 -0
  80. data/lib/better_auth/routes/user.rb +193 -0
  81. data/lib/better_auth/schema/sql.rb +191 -0
  82. data/lib/better_auth/schema.rb +275 -0
  83. data/lib/better_auth/session.rb +122 -0
  84. data/lib/better_auth/session_store.rb +91 -0
  85. data/lib/better_auth/social_providers/apple.rb +55 -0
  86. data/lib/better_auth/social_providers/base.rb +67 -0
  87. data/lib/better_auth/social_providers/discord.rb +59 -0
  88. data/lib/better_auth/social_providers/github.rb +59 -0
  89. data/lib/better_auth/social_providers/gitlab.rb +54 -0
  90. data/lib/better_auth/social_providers/google.rb +65 -0
  91. data/lib/better_auth/social_providers/microsoft_entra_id.rb +65 -0
  92. data/lib/better_auth/social_providers.rb +9 -0
  93. data/lib/better_auth/version.rb +1 -1
  94. data/lib/better_auth.rb +87 -2
  95. metadata +218 -21
  96. data/.ruby-version +0 -1
  97. data/.standard.yml +0 -12
  98. data/.vscode/settings.json +0 -22
  99. data/AGENTS.md +0 -50
  100. data/CLAUDE.md +0 -1
  101. data/CODE_OF_CONDUCT.md +0 -173
  102. data/CONTRIBUTING.md +0 -187
  103. data/Gemfile +0 -12
  104. data/Makefile +0 -207
  105. data/Rakefile +0 -25
  106. data/SECURITY.md +0 -28
  107. data/docker-compose.yml +0 -63
@@ -0,0 +1,518 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module BetterAuth
6
+ module Plugins
7
+ ADMIN_ERROR_CODES = {
8
+ "FAILED_TO_CREATE_USER" => "Failed to create user",
9
+ "USER_ALREADY_EXISTS" => "User already exists.",
10
+ "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL" => "User already exists. Use another email.",
11
+ "YOU_CANNOT_BAN_YOURSELF" => "You cannot ban yourself",
12
+ "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE" => "You are not allowed to change users role",
13
+ "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS" => "You are not allowed to create users",
14
+ "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS" => "You are not allowed to list users",
15
+ "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS" => "You are not allowed to list users sessions",
16
+ "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS" => "You are not allowed to ban users",
17
+ "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS" => "You are not allowed to impersonate users",
18
+ "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS" => "You are not allowed to revoke users sessions",
19
+ "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS" => "You are not allowed to delete users",
20
+ "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD" => "You are not allowed to set users password",
21
+ "BANNED_USER" => "You have been banned from this application",
22
+ "YOU_ARE_NOT_ALLOWED_TO_GET_USER" => "You are not allowed to get user",
23
+ "NO_DATA_TO_UPDATE" => "No data to update",
24
+ "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS" => "You are not allowed to update users",
25
+ "YOU_CANNOT_REMOVE_YOURSELF" => "You cannot remove yourself",
26
+ "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE" => "You are not allowed to set a non-existent role value",
27
+ "YOU_CANNOT_IMPERSONATE_ADMINS" => "You cannot impersonate admins",
28
+ "INVALID_ROLE_TYPE" => "Invalid role type"
29
+ }.freeze
30
+
31
+ ADMIN_DEFAULT_STATEMENTS = {
32
+ user: ["create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update"],
33
+ session: ["list", "revoke", "delete"]
34
+ }.freeze
35
+ ADMIN_DEFAULT_ROLE_STATEMENTS = {
36
+ user: ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update"],
37
+ session: ["list", "revoke", "delete"]
38
+ }.freeze
39
+
40
+ module_function
41
+
42
+ def admin(options = {})
43
+ config = admin_config(options)
44
+ Plugin.new(
45
+ id: "admin",
46
+ init: ->(_context) { {options: {database_hooks: admin_database_hooks(config)}} },
47
+ schema: AdminSchema.build(config[:schema]),
48
+ endpoints: {
49
+ set_role: admin_set_role_endpoint(config),
50
+ get_user: admin_get_user_endpoint(config),
51
+ create_user: admin_create_user_endpoint(config),
52
+ admin_update_user: admin_update_user_endpoint(config),
53
+ list_users: admin_list_users_endpoint(config),
54
+ list_user_sessions: admin_list_user_sessions_endpoint(config),
55
+ unban_user: admin_unban_user_endpoint(config),
56
+ ban_user: admin_ban_user_endpoint(config),
57
+ impersonate_user: admin_impersonate_user_endpoint(config),
58
+ stop_impersonating: admin_stop_impersonating_endpoint,
59
+ revoke_user_session: admin_revoke_user_session_endpoint(config),
60
+ revoke_user_sessions: admin_revoke_user_sessions_endpoint(config),
61
+ remove_user: admin_remove_user_endpoint(config),
62
+ set_user_password: admin_set_user_password_endpoint(config),
63
+ user_has_permission: admin_has_permission_endpoint(config)
64
+ },
65
+ hooks: {
66
+ after: [
67
+ {
68
+ matcher: ->(ctx) { ctx.path == "/list-sessions" },
69
+ handler: ->(ctx) { ctx.json(Array(ctx.returned).reject { |session| session["impersonatedBy"] || session[:impersonatedBy] }) }
70
+ }
71
+ ]
72
+ },
73
+ error_codes: ADMIN_ERROR_CODES,
74
+ options: config
75
+ )
76
+ end
77
+
78
+ def admin_config(options)
79
+ config = normalize_hash(options)
80
+ config[:roles_configured] = config.key?(:roles)
81
+ config[:default_role] ||= "user"
82
+ config[:admin_roles] = Array(config[:admin_roles] || ["admin"]).flat_map { |role| role.to_s.split(",") }
83
+ config[:banned_user_message] ||= "You have been banned from this application. Please contact support if you believe this is an error."
84
+ config[:impersonation_session_duration] ||= 60 * 60
85
+ config[:ac] ||= create_access_control(ADMIN_DEFAULT_STATEMENTS)
86
+ config[:roles] ||= admin_default_roles(config)
87
+ valid_roles = config[:roles].keys.map { |role| role.to_s.downcase }
88
+ invalid = config[:admin_roles].reject { |role| valid_roles.include?(role.to_s.downcase) }
89
+ raise Error, "Invalid admin roles: #{invalid.join(", ")}. Admin roles must be defined in the 'roles' configuration." if invalid.any?
90
+
91
+ config
92
+ end
93
+
94
+ def admin_default_roles(config = {})
95
+ ac = config[:ac] || create_access_control(ADMIN_DEFAULT_STATEMENTS)
96
+ {
97
+ "admin" => ac.new_role(ADMIN_DEFAULT_ROLE_STATEMENTS),
98
+ "user" => ac.new_role(user: [], session: [])
99
+ }
100
+ end
101
+
102
+ def admin_database_hooks(config)
103
+ {
104
+ user: {
105
+ create: {
106
+ before: lambda do |user, _ctx|
107
+ {data: {"role" => config[:default_role]}.merge(user)}
108
+ end
109
+ }
110
+ },
111
+ session: {
112
+ create: {
113
+ before: lambda do |session, ctx|
114
+ next unless ctx
115
+
116
+ user = ctx.context.internal_adapter.find_user_by_id(session["userId"] || session[:userId])
117
+ next unless user && user["banned"]
118
+
119
+ if user["banExpires"] && Time.parse(user["banExpires"].to_s) < Time.now
120
+ ctx.context.internal_adapter.update_user(user["id"], banned: false, banReason: nil, banExpires: nil, updatedAt: Time.now)
121
+ next
122
+ end
123
+
124
+ if ctx.path.to_s.start_with?("/callback", "/oauth2/callback")
125
+ error_url = ctx.context.options.on_api_error[:error_url] || "#{ctx.context.base_url}/error"
126
+ url = "#{error_url}?error=banned&error_description=#{URI.encode_www_form_component(config[:banned_user_message])}"
127
+ raise ctx.redirect(url)
128
+ end
129
+
130
+ raise APIError.new("FORBIDDEN", message: config[:banned_user_message], code: "BANNED_USER")
131
+ end
132
+ }
133
+ }
134
+ }
135
+ end
136
+
137
+ def admin_set_role_endpoint(config)
138
+ Endpoint.new(path: "/admin/set-role", method: "POST") do |ctx|
139
+ admin_require_permission!(ctx, config, {user: ["set-role"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE"))
140
+ body = normalize_hash(ctx.body)
141
+ user_id = body[:user_id].to_s
142
+ raise APIError.new("BAD_REQUEST", message: "userId is required") if user_id.empty?
143
+ update = {role: admin_validate_roles!(body[:role], config)}
144
+ user = ctx.context.internal_adapter.update_user(user_id, update)
145
+ ctx.json({user: Schema.parse_output(ctx.context.options, "user", user || {})})
146
+ end
147
+ end
148
+
149
+ def admin_get_user_endpoint(config)
150
+ Endpoint.new(path: "/admin/get-user", method: "GET") do |ctx|
151
+ admin_require_permission!(ctx, config, {user: ["get"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_GET_USER"))
152
+ query = normalize_hash(ctx.query)
153
+ user = if query[:id] || query[:user_id]
154
+ ctx.context.internal_adapter.find_user_by_id(query[:id] || query[:user_id])
155
+ elsif query[:email]
156
+ ctx.context.internal_adapter.find_user_by_email(query[:email])&.fetch(:user)
157
+ end
158
+ raise APIError.new("NOT_FOUND", message: BASE_ERROR_CODES.fetch("USER_NOT_FOUND")) unless user
159
+ ctx.json(Schema.parse_output(ctx.context.options, "user", user))
160
+ end
161
+ end
162
+
163
+ def admin_create_user_endpoint(config)
164
+ Endpoint.new(path: "/admin/create-user", method: "POST") do |ctx|
165
+ session = Routes.current_session(ctx, allow_nil: true)
166
+ if session
167
+ unless admin_permission?(session[:user], session[:user]["role"], {user: ["create"]}, config)
168
+ raise APIError.new("FORBIDDEN", message: ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS"))
169
+ end
170
+ elsif !ctx.headers.empty?
171
+ raise APIError.new("UNAUTHORIZED")
172
+ end
173
+
174
+ body = normalize_hash(ctx.body)
175
+ email = body[:email].to_s.downcase
176
+ raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES.fetch("INVALID_EMAIL")) unless Routes::EMAIL_PATTERN.match?(email)
177
+
178
+ if ctx.context.internal_adapter.find_user_by_email(email)
179
+ raise APIError.new("BAD_REQUEST", message: ADMIN_ERROR_CODES.fetch("USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL"))
180
+ end
181
+ data = normalize_hash(body[:data]).each_with_object({}) { |(key, value), result| result[Schema.storage_key(key)] = value }
182
+ user = ctx.context.internal_adapter.create_user(data.merge(
183
+ name: body[:name].to_s,
184
+ email: email,
185
+ role: admin_validate_roles!(body[:role] || config[:default_role], config)
186
+ ).merge(body.key?(:image) ? {image: body[:image]} : {}))
187
+ raise APIError.new("INTERNAL_SERVER_ERROR", message: ADMIN_ERROR_CODES.fetch("FAILED_TO_CREATE_USER")) unless user
188
+
189
+ if body[:password].to_s != ""
190
+ ctx.context.internal_adapter.link_account(userId: user["id"], providerId: "credential", accountId: user["id"], password: Routes.hash_password(ctx, body[:password]))
191
+ end
192
+ ctx.json({user: Schema.parse_output(ctx.context.options, "user", user)})
193
+ end
194
+ end
195
+
196
+ def admin_update_user_endpoint(config)
197
+ Endpoint.new(path: "/admin/update-user", method: "POST") do |ctx|
198
+ admin_require_permission!(ctx, config, {user: ["update"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS"))
199
+ body = normalize_hash(ctx.body)
200
+ data = normalize_hash(body[:data] || body).except(:user_id, :data)
201
+ raise APIError.new("BAD_REQUEST", message: ADMIN_ERROR_CODES.fetch("NO_DATA_TO_UPDATE")) if data.empty?
202
+ if data.key?(:role)
203
+ admin_require_permission!(ctx, config, {user: ["set-role"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE"))
204
+ data[:role] = admin_validate_roles!(data[:role], config)
205
+ end
206
+ user = ctx.context.internal_adapter.update_user(body[:user_id], data)
207
+ ctx.json(Schema.parse_output(ctx.context.options, "user", user))
208
+ end
209
+ end
210
+
211
+ def admin_list_users_endpoint(config)
212
+ Endpoint.new(path: "/admin/list-users", method: "GET") do |ctx|
213
+ admin_require_permission!(ctx, config, {user: ["list"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_LIST_USERS"))
214
+ query = normalize_hash(ctx.query)
215
+ where = admin_user_where(query)
216
+ sort_by = admin_user_sort(query)
217
+ limit = query.key?(:limit) ? query[:limit].to_i : nil
218
+ offset = query.key?(:offset) ? query[:offset].to_i : nil
219
+ users = ctx.context.internal_adapter.list_users(limit: limit, offset: offset, sort_by: sort_by, where: where)
220
+ total = ctx.context.internal_adapter.count_total_users(where: where)
221
+ ctx.json({
222
+ users: users.map { |user| Schema.parse_output(ctx.context.options, "user", user) },
223
+ total: total,
224
+ limit: limit,
225
+ offset: offset
226
+ })
227
+ end
228
+ end
229
+
230
+ def admin_list_user_sessions_endpoint(config)
231
+ Endpoint.new(path: "/admin/list-user-sessions", method: "POST") do |ctx|
232
+ admin_require_permission!(ctx, config, {session: ["list"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS"))
233
+ sessions = ctx.context.internal_adapter.list_sessions(normalize_hash(ctx.body)[:user_id])
234
+ ctx.json({sessions: sessions.map { |session| Schema.parse_output(ctx.context.options, "session", session) }})
235
+ end
236
+ end
237
+
238
+ def admin_ban_user_endpoint(config)
239
+ Endpoint.new(path: "/admin/ban-user", method: "POST") do |ctx|
240
+ session = admin_require_permission!(ctx, config, {user: ["ban"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_BAN_USERS"))
241
+ body = normalize_hash(ctx.body)
242
+ found = ctx.context.internal_adapter.find_user_by_id(body[:user_id])
243
+ raise APIError.new("NOT_FOUND", message: BASE_ERROR_CODES.fetch("USER_NOT_FOUND")) unless found
244
+ raise APIError.new("BAD_REQUEST", message: ADMIN_ERROR_CODES.fetch("YOU_CANNOT_BAN_YOURSELF")) if body[:user_id] == session[:user]["id"]
245
+ expires_in = body[:ban_expires_in] || config[:default_ban_expires_in]
246
+ user = ctx.context.internal_adapter.update_user(body[:user_id], banned: true, banReason: body[:ban_reason] || config[:default_ban_reason] || "No reason", banExpires: expires_in ? Time.now + expires_in.to_i : nil, updatedAt: Time.now)
247
+ ctx.context.internal_adapter.delete_sessions(body[:user_id])
248
+ ctx.json({user: Schema.parse_output(ctx.context.options, "user", user)})
249
+ end
250
+ end
251
+
252
+ def admin_unban_user_endpoint(config)
253
+ Endpoint.new(path: "/admin/unban-user", method: "POST") do |ctx|
254
+ admin_require_permission!(ctx, config, {user: ["ban"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_BAN_USERS"))
255
+ user = ctx.context.internal_adapter.update_user(normalize_hash(ctx.body)[:user_id], banned: false, banReason: nil, banExpires: nil, updatedAt: Time.now)
256
+ ctx.json({user: Schema.parse_output(ctx.context.options, "user", user)})
257
+ end
258
+ end
259
+
260
+ def admin_impersonate_user_endpoint(config)
261
+ Endpoint.new(path: "/admin/impersonate-user", method: "POST") do |ctx|
262
+ session = admin_require_permission!(ctx, config, {user: ["impersonate"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS"))
263
+ body = normalize_hash(ctx.body)
264
+ target = ctx.context.internal_adapter.find_user_by_id(body[:user_id])
265
+ raise APIError.new("NOT_FOUND", message: "User not found") unless target
266
+ can_impersonate_admins = config[:allow_impersonating_admins] ||
267
+ admin_permission?(session[:user], session[:user]["role"], {user: ["impersonate-admins"]}, config)
268
+ if !can_impersonate_admins && admin_user?(target, config)
269
+ raise APIError.new("FORBIDDEN", message: ADMIN_ERROR_CODES.fetch("YOU_CANNOT_IMPERSONATE_ADMINS"))
270
+ end
271
+ impersonated = ctx.context.internal_adapter.create_session(target["id"], true, {impersonatedBy: session[:user]["id"], expiresAt: Time.now + config[:impersonation_session_duration].to_i}, true, ctx)
272
+ raise APIError.new("INTERNAL_SERVER_ERROR", message: ADMIN_ERROR_CODES.fetch("FAILED_TO_CREATE_USER")) unless impersonated
273
+
274
+ dont_remember_cookie = ctx.get_signed_cookie(ctx.context.auth_cookies[:dont_remember].name, ctx.context.secret)
275
+ Cookies.delete_session_cookie(ctx)
276
+ admin_cookie = ctx.context.create_auth_cookie("admin_session")
277
+ ctx.set_signed_cookie(admin_cookie.name, "#{session[:session]["token"]}:#{dont_remember_cookie}", ctx.context.secret, ctx.context.auth_cookies[:session_token].attributes)
278
+ Cookies.set_session_cookie(ctx, {session: impersonated, user: target}, true)
279
+ ctx.json({
280
+ session: Schema.parse_output(ctx.context.options, "session", impersonated),
281
+ user: Schema.parse_output(ctx.context.options, "user", target)
282
+ })
283
+ end
284
+ end
285
+
286
+ def admin_stop_impersonating_endpoint
287
+ Endpoint.new(path: "/admin/stop-impersonating", method: "POST") do |ctx|
288
+ session = Routes.current_session(ctx, sensitive: true)
289
+ admin_id = session[:session]["impersonatedBy"]
290
+ raise APIError.new("BAD_REQUEST", message: "You are not impersonating anyone") unless admin_id
291
+ admin = ctx.context.internal_adapter.find_user_by_id(admin_id)
292
+ raise APIError.new("INTERNAL_SERVER_ERROR", message: "Failed to find user") unless admin
293
+
294
+ admin_cookie = ctx.context.create_auth_cookie("admin_session")
295
+ admin_cookie_value = ctx.get_signed_cookie(admin_cookie.name, ctx.context.secret)
296
+ raise APIError.new("INTERNAL_SERVER_ERROR", message: "Failed to find admin session") unless admin_cookie_value
297
+
298
+ admin_session_token, dont_remember_cookie = admin_cookie_value.split(":", 2)
299
+ admin_session = ctx.context.internal_adapter.find_session(admin_session_token)
300
+ if !admin_session || admin_session[:session]["userId"] != admin["id"]
301
+ raise APIError.new("INTERNAL_SERVER_ERROR", message: "Failed to find admin session")
302
+ end
303
+
304
+ ctx.context.internal_adapter.delete_session(session[:session]["token"])
305
+ Cookies.set_session_cookie(ctx, admin_session, !dont_remember_cookie.to_s.empty?)
306
+ Cookies.expire_cookie(ctx, admin_cookie)
307
+ ctx.json({
308
+ session: Schema.parse_output(ctx.context.options, "session", admin_session[:session]),
309
+ user: Schema.parse_output(ctx.context.options, "user", admin_session[:user])
310
+ })
311
+ end
312
+ end
313
+
314
+ def admin_revoke_user_session_endpoint(config)
315
+ Endpoint.new(path: "/admin/revoke-user-session", method: "POST") do |ctx|
316
+ admin_require_permission!(ctx, config, {session: ["revoke"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS"))
317
+ ctx.context.internal_adapter.delete_session(normalize_hash(ctx.body)[:session_token])
318
+ ctx.json({success: true})
319
+ end
320
+ end
321
+
322
+ def admin_revoke_user_sessions_endpoint(config)
323
+ Endpoint.new(path: "/admin/revoke-user-sessions", method: "POST") do |ctx|
324
+ admin_require_permission!(ctx, config, {session: ["revoke"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS"))
325
+ ctx.context.internal_adapter.delete_sessions(normalize_hash(ctx.body)[:user_id])
326
+ ctx.json({success: true})
327
+ end
328
+ end
329
+
330
+ def admin_remove_user_endpoint(config)
331
+ Endpoint.new(path: "/admin/remove-user", method: "POST") do |ctx|
332
+ session = admin_require_permission!(ctx, config, {user: ["delete"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS"))
333
+ user_id = normalize_hash(ctx.body)[:user_id]
334
+ raise APIError.new("BAD_REQUEST", message: ADMIN_ERROR_CODES.fetch("YOU_CANNOT_REMOVE_YOURSELF")) if user_id == session[:user]["id"]
335
+ raise APIError.new("NOT_FOUND", message: BASE_ERROR_CODES.fetch("USER_NOT_FOUND")) unless ctx.context.internal_adapter.find_user_by_id(user_id)
336
+ ctx.context.internal_adapter.delete_user(user_id)
337
+ ctx.json({success: true})
338
+ end
339
+ end
340
+
341
+ def admin_set_user_password_endpoint(config)
342
+ Endpoint.new(path: "/admin/set-user-password", method: "POST") do |ctx|
343
+ admin_require_permission!(ctx, config, {user: ["set-password"]}, ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD"))
344
+ body = normalize_hash(ctx.body)
345
+ user_id = body[:user_id].to_s
346
+ password = body[:new_password].to_s
347
+ raise APIError.new("BAD_REQUEST", message: "userId is required") if user_id.empty?
348
+ min = ctx.context.options.email_and_password[:min_password_length]
349
+ max = ctx.context.options.email_and_password[:max_password_length]
350
+ raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES.fetch("PASSWORD_TOO_SHORT")) if password.length < min
351
+ raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES.fetch("PASSWORD_TOO_LONG")) if password.length > max
352
+ ctx.context.internal_adapter.update_password(user_id, Routes.hash_password(ctx, password))
353
+ ctx.json({status: true})
354
+ end
355
+ end
356
+
357
+ def admin_has_permission_endpoint(config)
358
+ Endpoint.new(path: "/admin/has-permission", method: "POST") do |ctx|
359
+ session = Routes.current_session(ctx, allow_nil: true)
360
+ body = normalize_hash(ctx.body)
361
+ permissions = body[:permissions] || body[:permission]
362
+ unless permissions
363
+ raise APIError.new("BAD_REQUEST", message: "invalid permission check. no permission(s) were passed.")
364
+ end
365
+
366
+ if session
367
+ user = session[:user]
368
+ role = user["role"]
369
+ elsif !ctx.headers.empty?
370
+ raise APIError.new("UNAUTHORIZED")
371
+ elsif body.key?(:role)
372
+ role = body[:role]
373
+ user = {"id" => body[:user_id].to_s, "role" => role}
374
+ elsif body.key?(:user_id)
375
+ user_id = body[:user_id].to_s
376
+ raise APIError.new("BAD_REQUEST", message: "user id or role is required") if user_id.empty?
377
+
378
+ user = ctx.context.internal_adapter.find_user_by_id(user_id)
379
+ raise APIError.new("BAD_REQUEST", message: "user not found") unless user
380
+
381
+ role = user["role"]
382
+ else
383
+ raise APIError.new("BAD_REQUEST", message: "user id or role is required")
384
+ end
385
+ ctx.json({error: nil, success: admin_permission?(user, role, permissions, config)})
386
+ end
387
+ end
388
+
389
+ def admin_require_permission!(ctx, config, permissions, message)
390
+ session = Routes.current_session(ctx, sensitive: true)
391
+ return session if admin_permission?(session[:user], session[:user]["role"], permissions, config)
392
+
393
+ raise APIError.new("FORBIDDEN", message: message)
394
+ end
395
+
396
+ def admin_permission?(user, role_string, permissions, config)
397
+ return true if user && Array(config[:admin_user_ids]).map(&:to_s).include?(user["id"].to_s)
398
+ return false unless permissions
399
+
400
+ roles = (config[:roles] || admin_default_roles(config)).transform_keys(&:to_s)
401
+ selected_roles = role_string.to_s.empty? ? [config[:default_role].to_s] : role_string.to_s.split(",")
402
+ selected_roles.any? do |role|
403
+ admin_role_for(roles, role)&.authorize(permissions || {})&.fetch(:success, false)
404
+ end
405
+ end
406
+
407
+ def admin_user?(user, config)
408
+ return true if Array(config[:admin_user_ids]).map(&:to_s).include?(user["id"].to_s)
409
+
410
+ admin_roles = config[:admin_roles].map { |role| role.to_s.downcase }
411
+ user["role"].to_s.split(",").any? { |role| admin_roles.include?(role.to_s.downcase) }
412
+ end
413
+
414
+ def admin_parse_roles(roles)
415
+ Array(roles).join(",")
416
+ end
417
+
418
+ def admin_validate_roles!(roles, config)
419
+ unless Array(roles).all? { |role| role.is_a?(String) || role.is_a?(Symbol) }
420
+ raise APIError.new("BAD_REQUEST", message: ADMIN_ERROR_CODES.fetch("INVALID_ROLE_TYPE"))
421
+ end
422
+
423
+ parsed = admin_parse_roles(roles)
424
+ if config[:roles_configured]
425
+ defined_roles = (config[:roles] || {}).transform_keys(&:to_s)
426
+ invalid = parsed.split(",", -1).reject { |role| admin_role_for(defined_roles, role) }
427
+ if invalid.any?
428
+ raise APIError.new("BAD_REQUEST", message: ADMIN_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE"))
429
+ end
430
+ end
431
+
432
+ parsed
433
+ end
434
+
435
+ def admin_role_for(roles, role)
436
+ roles[role.to_s] || roles.find { |key, _value| key.to_s.downcase == role.to_s.downcase }&.last
437
+ end
438
+
439
+ def admin_user_where(query)
440
+ where = []
441
+ search_value = query[:search_value]
442
+ if search_value && !search_value.to_s.empty?
443
+ where << {
444
+ field: query[:search_field] || "email",
445
+ operator: query[:search_operator] || "contains",
446
+ value: search_value
447
+ }
448
+ end
449
+
450
+ filter_value_defined = query.key?(:filter_value) || (query[:filter].is_a?(Hash) && query[:filter].key?(:value))
451
+ if filter_value_defined
452
+ filter_field = query[:filter_field] || query.dig(:filter, :field) || "email"
453
+ where << {
454
+ field: (filter_field.to_s == "_id") ? "id" : filter_field,
455
+ operator: query[:filter_operator] || query.dig(:filter, :operator) || "eq",
456
+ value: query.key?(:filter_value) ? query[:filter_value] : query.dig(:filter, :value)
457
+ }
458
+ end
459
+
460
+ where
461
+ end
462
+
463
+ def admin_user_sort(query)
464
+ sort_field = query[:sort_by] || query[:sort_field]
465
+ return nil unless sort_field
466
+
467
+ {
468
+ field: sort_field,
469
+ direction: query[:sort_direction] || query[:sort_order] || "asc"
470
+ }
471
+ end
472
+
473
+ def admin_filter_users(users, query)
474
+ result = users
475
+ search_value = query[:search_value].to_s
476
+ if !search_value.empty?
477
+ field = (query[:search_field] || "email").to_s
478
+ result = result.select { |user| user[field].to_s.downcase.include?(search_value.downcase) }
479
+ end
480
+ filter_field = (query[:filter_field] || query.dig(:filter, :field)).to_s
481
+ if !filter_field.empty?
482
+ filter_value = if query.key?(:filter_value)
483
+ query[:filter_value]
484
+ else
485
+ query.dig(:filter, :value)
486
+ end
487
+ operator = (query[:filter_operator] || query.dig(:filter, :operator) || "eq").to_s
488
+ field = (filter_field == "_id") ? "id" : Schema.storage_key(filter_field)
489
+ result = result.select do |user|
490
+ current = user[field]
491
+ case operator
492
+ when "ne" then current != filter_value
493
+ when "contains" then current.to_s.include?(filter_value.to_s)
494
+ else current == filter_value
495
+ end
496
+ end
497
+ end
498
+ result
499
+ end
500
+
501
+ def admin_sort_users(users, query)
502
+ sort_field = query[:sort_by] || query[:sort_field]
503
+ return users unless sort_field
504
+
505
+ field = Schema.storage_key(sort_field)
506
+ sorted = users.sort_by { |user| user[field].to_s }
507
+ direction = (query[:sort_direction] || query[:sort_order] || "asc").to_s
508
+ (direction.downcase == "desc") ? sorted.reverse : sorted
509
+ end
510
+
511
+ def admin_paginate_users(users, query)
512
+ offset = query[:offset].to_i
513
+ limit = query[:limit]
514
+ result = offset.positive? ? users.drop(offset) : users
515
+ limit ? result.first(limit.to_i) : result
516
+ end
517
+ end
518
+ end