better_auth 0.1.1 → 0.3.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 +23 -0
- data/README.md +110 -18
- data/lib/better_auth/adapters/base.rb +49 -0
- data/lib/better_auth/adapters/internal_adapter.rb +589 -0
- data/lib/better_auth/adapters/memory.rb +235 -0
- data/lib/better_auth/adapters/mongodb.rb +9 -0
- data/lib/better_auth/adapters/mssql.rb +42 -0
- data/lib/better_auth/adapters/mysql.rb +33 -0
- data/lib/better_auth/adapters/postgres.rb +17 -0
- data/lib/better_auth/adapters/sql.rb +441 -0
- data/lib/better_auth/adapters/sqlite.rb +20 -0
- data/lib/better_auth/api.rb +226 -0
- data/lib/better_auth/api_error.rb +53 -0
- data/lib/better_auth/auth.rb +42 -0
- data/lib/better_auth/configuration.rb +399 -0
- data/lib/better_auth/context.rb +211 -0
- data/lib/better_auth/cookies.rb +278 -0
- data/lib/better_auth/core.rb +37 -1
- data/lib/better_auth/crypto/jwe.rb +76 -0
- data/lib/better_auth/crypto.rb +191 -0
- data/lib/better_auth/database_hooks.rb +114 -0
- data/lib/better_auth/endpoint.rb +326 -0
- data/lib/better_auth/error.rb +52 -0
- data/lib/better_auth/middleware/origin_check.rb +128 -0
- data/lib/better_auth/password.rb +120 -0
- data/lib/better_auth/plugin.rb +142 -0
- data/lib/better_auth/plugin_context.rb +16 -0
- data/lib/better_auth/plugin_registry.rb +67 -0
- data/lib/better_auth/plugins/access.rb +87 -0
- data/lib/better_auth/plugins/additional_fields.rb +29 -0
- data/lib/better_auth/plugins/admin/schema.rb +28 -0
- data/lib/better_auth/plugins/admin.rb +518 -0
- data/lib/better_auth/plugins/anonymous.rb +198 -0
- data/lib/better_auth/plugins/api_key.rb +16 -0
- data/lib/better_auth/plugins/bearer.rb +128 -0
- data/lib/better_auth/plugins/captcha.rb +159 -0
- data/lib/better_auth/plugins/custom_session.rb +84 -0
- data/lib/better_auth/plugins/device_authorization.rb +302 -0
- data/lib/better_auth/plugins/email_otp.rb +536 -0
- data/lib/better_auth/plugins/expo.rb +88 -0
- data/lib/better_auth/plugins/generic_oauth.rb +780 -0
- data/lib/better_auth/plugins/have_i_been_pwned.rb +94 -0
- data/lib/better_auth/plugins/jwt.rb +482 -0
- data/lib/better_auth/plugins/last_login_method.rb +92 -0
- data/lib/better_auth/plugins/magic_link.rb +181 -0
- data/lib/better_auth/plugins/mcp.rb +342 -0
- data/lib/better_auth/plugins/multi_session.rb +173 -0
- data/lib/better_auth/plugins/oauth_protocol.rb +694 -0
- data/lib/better_auth/plugins/oauth_provider.rb +16 -0
- data/lib/better_auth/plugins/oauth_proxy.rb +257 -0
- data/lib/better_auth/plugins/oidc_provider.rb +597 -0
- data/lib/better_auth/plugins/one_tap.rb +154 -0
- data/lib/better_auth/plugins/one_time_token.rb +106 -0
- data/lib/better_auth/plugins/open_api.rb +489 -0
- data/lib/better_auth/plugins/organization/schema.rb +106 -0
- data/lib/better_auth/plugins/organization.rb +995 -0
- data/lib/better_auth/plugins/passkey.rb +16 -0
- data/lib/better_auth/plugins/phone_number.rb +321 -0
- data/lib/better_auth/plugins/scim.rb +16 -0
- data/lib/better_auth/plugins/siwe.rb +242 -0
- data/lib/better_auth/plugins/sso.rb +16 -0
- data/lib/better_auth/plugins/stripe.rb +16 -0
- data/lib/better_auth/plugins/two_factor.rb +514 -0
- data/lib/better_auth/plugins/username.rb +278 -0
- data/lib/better_auth/plugins.rb +46 -0
- data/lib/better_auth/rate_limiter.rb +232 -0
- data/lib/better_auth/request_ip.rb +70 -0
- data/lib/better_auth/router.rb +378 -0
- data/lib/better_auth/routes/account.rb +211 -0
- data/lib/better_auth/routes/email_verification.rb +111 -0
- data/lib/better_auth/routes/error.rb +102 -0
- data/lib/better_auth/routes/ok.rb +15 -0
- data/lib/better_auth/routes/password.rb +183 -0
- data/lib/better_auth/routes/session.rb +160 -0
- data/lib/better_auth/routes/sign_in.rb +90 -0
- data/lib/better_auth/routes/sign_out.rb +15 -0
- data/lib/better_auth/routes/sign_up.rb +196 -0
- data/lib/better_auth/routes/social.rb +367 -0
- data/lib/better_auth/routes/user.rb +205 -0
- data/lib/better_auth/schema/sql.rb +202 -0
- data/lib/better_auth/schema.rb +291 -0
- data/lib/better_auth/session.rb +122 -0
- data/lib/better_auth/session_store.rb +91 -0
- data/lib/better_auth/social_providers/apple.rb +91 -0
- data/lib/better_auth/social_providers/atlassian.rb +32 -0
- data/lib/better_auth/social_providers/base.rb +325 -0
- data/lib/better_auth/social_providers/cognito.rb +32 -0
- data/lib/better_auth/social_providers/discord.rb +81 -0
- data/lib/better_auth/social_providers/dropbox.rb +33 -0
- data/lib/better_auth/social_providers/facebook.rb +35 -0
- data/lib/better_auth/social_providers/figma.rb +31 -0
- data/lib/better_auth/social_providers/github.rb +74 -0
- data/lib/better_auth/social_providers/gitlab.rb +67 -0
- data/lib/better_auth/social_providers/google.rb +90 -0
- data/lib/better_auth/social_providers/huggingface.rb +31 -0
- data/lib/better_auth/social_providers/kakao.rb +32 -0
- data/lib/better_auth/social_providers/kick.rb +32 -0
- data/lib/better_auth/social_providers/line.rb +33 -0
- data/lib/better_auth/social_providers/linear.rb +44 -0
- data/lib/better_auth/social_providers/linkedin.rb +30 -0
- data/lib/better_auth/social_providers/microsoft_entra_id.rb +137 -0
- data/lib/better_auth/social_providers/naver.rb +31 -0
- data/lib/better_auth/social_providers/notion.rb +33 -0
- data/lib/better_auth/social_providers/paybin.rb +31 -0
- data/lib/better_auth/social_providers/paypal.rb +36 -0
- data/lib/better_auth/social_providers/polar.rb +31 -0
- data/lib/better_auth/social_providers/railway.rb +49 -0
- data/lib/better_auth/social_providers/reddit.rb +32 -0
- data/lib/better_auth/social_providers/roblox.rb +31 -0
- data/lib/better_auth/social_providers/salesforce.rb +38 -0
- data/lib/better_auth/social_providers/slack.rb +30 -0
- data/lib/better_auth/social_providers/spotify.rb +31 -0
- data/lib/better_auth/social_providers/tiktok.rb +35 -0
- data/lib/better_auth/social_providers/twitch.rb +39 -0
- data/lib/better_auth/social_providers/twitter.rb +32 -0
- data/lib/better_auth/social_providers/vercel.rb +47 -0
- data/lib/better_auth/social_providers/vk.rb +34 -0
- data/lib/better_auth/social_providers/wechat.rb +104 -0
- data/lib/better_auth/social_providers/zoom.rb +31 -0
- data/lib/better_auth/social_providers.rb +38 -0
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +86 -2
- metadata +233 -21
- data/.ruby-version +0 -1
- data/.standard.yml +0 -12
- data/.vscode/settings.json +0 -22
- data/AGENTS.md +0 -50
- data/CLAUDE.md +0 -1
- data/CODE_OF_CONDUCT.md +0 -173
- data/CONTRIBUTING.md +0 -187
- data/Gemfile +0 -12
- data/Makefile +0 -207
- data/Rakefile +0 -25
- data/SECURITY.md +0 -28
- data/docker-compose.yml +0 -63
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Plugins
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def open_api(options = {})
|
|
8
|
+
config = {path: "/reference", theme: "default"}.merge(normalize_hash(options))
|
|
9
|
+
|
|
10
|
+
Plugin.new(
|
|
11
|
+
id: "open-api",
|
|
12
|
+
endpoints: {
|
|
13
|
+
generate_open_api_schema: Endpoint.new(path: "/open-api/generate-schema", method: "GET") do |ctx|
|
|
14
|
+
ctx.json(open_api_schema(ctx.context))
|
|
15
|
+
end,
|
|
16
|
+
open_api_reference: Endpoint.new(path: config[:path], method: "GET", metadata: {hide: true}) do |ctx|
|
|
17
|
+
raise APIError.new("NOT_FOUND") if config[:disable_default_reference]
|
|
18
|
+
|
|
19
|
+
[200, {"content-type" => "text/html"}, [open_api_html(open_api_schema(ctx.context), config)]]
|
|
20
|
+
end
|
|
21
|
+
},
|
|
22
|
+
options: config
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def open_api_schema(context)
|
|
27
|
+
{
|
|
28
|
+
openapi: "3.1.1",
|
|
29
|
+
info: {
|
|
30
|
+
title: "Better Auth",
|
|
31
|
+
description: "API Reference for your Better Auth Instance",
|
|
32
|
+
version: "1.1.0"
|
|
33
|
+
},
|
|
34
|
+
components: {
|
|
35
|
+
schemas: open_api_components(context.options),
|
|
36
|
+
securitySchemes: open_api_security_schemes
|
|
37
|
+
},
|
|
38
|
+
security: [
|
|
39
|
+
{
|
|
40
|
+
apiKeyCookie: [],
|
|
41
|
+
bearerAuth: []
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
servers: [
|
|
45
|
+
{
|
|
46
|
+
url: context.base_url
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
tags: [
|
|
50
|
+
{
|
|
51
|
+
name: "Default",
|
|
52
|
+
description: "Default endpoints that are included with Better Auth by default. These endpoints are not part of any plugin."
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
paths: open_api_paths(open_api_endpoints(context.options), context.options)
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def open_api_endpoints(options)
|
|
60
|
+
Core.base_endpoints.map { |key, endpoint| [key, endpoint, "Default"] } +
|
|
61
|
+
options.plugins.flat_map do |plugin|
|
|
62
|
+
next [] if plugin.id == "open-api"
|
|
63
|
+
|
|
64
|
+
tag = plugin.id.to_s.split("-").map(&:capitalize).join("-")
|
|
65
|
+
plugin.endpoints.map { |key, endpoint| [key, endpoint, tag] }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def open_api_security_schemes
|
|
70
|
+
{
|
|
71
|
+
apiKeyCookie: {
|
|
72
|
+
type: "apiKey",
|
|
73
|
+
in: "cookie",
|
|
74
|
+
name: "apiKeyCookie",
|
|
75
|
+
description: "API Key authentication via cookie"
|
|
76
|
+
},
|
|
77
|
+
bearerAuth: {
|
|
78
|
+
type: "http",
|
|
79
|
+
scheme: "bearer",
|
|
80
|
+
description: "Bearer token authentication"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def open_api_paths(endpoints, options)
|
|
86
|
+
disabled_paths = Array(options.disabled_paths).map(&:to_s)
|
|
87
|
+
endpoints.each_with_object({}) do |(_key, endpoint, tag), paths|
|
|
88
|
+
next unless endpoint.path
|
|
89
|
+
next if endpoint.metadata[:hide] || endpoint.metadata[:SERVER_ONLY] || endpoint.metadata[:server_only]
|
|
90
|
+
next if disabled_paths.include?(endpoint.path)
|
|
91
|
+
|
|
92
|
+
path = open_api_path(endpoint.path)
|
|
93
|
+
paths[path] ||= {}
|
|
94
|
+
endpoint.methods.reject { |method| method == "*" }.each do |method|
|
|
95
|
+
paths[path][method.downcase.to_sym] = open_api_operation(endpoint, method, tag)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def open_api_path(path)
|
|
101
|
+
path.split("/").map { |part| part.start_with?(":") ? "{#{part.delete_prefix(":")}}" : part }.join("/")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def open_api_operation(endpoint, method, tag)
|
|
105
|
+
metadata = endpoint.metadata[:openapi] || {}
|
|
106
|
+
operation = {
|
|
107
|
+
tags: Array(metadata[:tags] || [tag]),
|
|
108
|
+
description: metadata[:description] || route_description(endpoint.path, method),
|
|
109
|
+
operationId: metadata.key?(:operationId) ? metadata[:operationId] : route_operation_id(endpoint.path, method),
|
|
110
|
+
security: [
|
|
111
|
+
{
|
|
112
|
+
bearerAuth: []
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
parameters: metadata[:parameters] || [],
|
|
116
|
+
responses: open_api_responses(metadata[:responses] || route_responses(endpoint.path, method))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if %w[POST PATCH PUT].include?(method)
|
|
120
|
+
operation[:requestBody] = metadata[:requestBody] || route_request_body(endpoint.path, method) || empty_request_body
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
operation
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def route_description(path, method)
|
|
127
|
+
route_open_api_metadata(path, method)[:description]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def route_operation_id(path, method)
|
|
131
|
+
route_open_api_metadata(path, method)[:operationId]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def route_request_body(path, method)
|
|
135
|
+
route_open_api_metadata(path, method)[:requestBody]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def route_responses(path, method)
|
|
139
|
+
route_open_api_metadata(path, method)[:responses]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def route_open_api_metadata(path, method)
|
|
143
|
+
case [path, method.to_s.upcase]
|
|
144
|
+
when ["/change-email", "POST"]
|
|
145
|
+
{
|
|
146
|
+
operationId: "changeEmail",
|
|
147
|
+
requestBody: {
|
|
148
|
+
required: true,
|
|
149
|
+
content: {
|
|
150
|
+
"application/json" => {
|
|
151
|
+
schema: object_schema(
|
|
152
|
+
{
|
|
153
|
+
callbackURL: {type: ["string", "null"], description: "The URL to redirect to after email verification"},
|
|
154
|
+
newEmail: {type: "string", description: "The new email address to set must be a valid email address"}
|
|
155
|
+
},
|
|
156
|
+
required: ["newEmail"]
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
responses: {
|
|
162
|
+
"200" => {
|
|
163
|
+
description: "Email change request processed successfully",
|
|
164
|
+
content: {
|
|
165
|
+
"application/json" => {
|
|
166
|
+
schema: object_schema(
|
|
167
|
+
{
|
|
168
|
+
message: {
|
|
169
|
+
type: "string",
|
|
170
|
+
nullable: true,
|
|
171
|
+
enum: ["Email updated", "Verification email sent"],
|
|
172
|
+
description: "Status message of the email change process"
|
|
173
|
+
},
|
|
174
|
+
status: {type: "boolean", description: "Indicates if the request was successful"},
|
|
175
|
+
user: {type: "object", "$ref": "#/components/schemas/User"}
|
|
176
|
+
},
|
|
177
|
+
required: ["status"]
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"422" => error_response("Unprocessable Entity. Email already exists")
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
when ["/change-password", "POST"]
|
|
186
|
+
{
|
|
187
|
+
description: "Change the password of the user",
|
|
188
|
+
operationId: "changePassword",
|
|
189
|
+
requestBody: {
|
|
190
|
+
required: true,
|
|
191
|
+
content: {
|
|
192
|
+
"application/json" => {
|
|
193
|
+
schema: object_schema(
|
|
194
|
+
{
|
|
195
|
+
newPassword: {type: "string", description: "The new password to set"},
|
|
196
|
+
currentPassword: {type: "string", description: "The current password is required"},
|
|
197
|
+
revokeOtherSessions: {type: ["boolean", "null"], description: "Must be a boolean value"}
|
|
198
|
+
},
|
|
199
|
+
required: ["newPassword", "currentPassword"]
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
responses: {
|
|
205
|
+
"200" => {
|
|
206
|
+
description: "Password successfully changed",
|
|
207
|
+
content: {
|
|
208
|
+
"application/json" => {
|
|
209
|
+
schema: object_schema(
|
|
210
|
+
{
|
|
211
|
+
token: {type: "string", nullable: true, description: "New session token if other sessions were revoked"},
|
|
212
|
+
user: open_api_user_response_schema
|
|
213
|
+
},
|
|
214
|
+
required: ["user"]
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
when ["/sign-in/email", "POST"]
|
|
222
|
+
{
|
|
223
|
+
description: "Sign in with email and password",
|
|
224
|
+
operationId: "signInEmail",
|
|
225
|
+
requestBody: {
|
|
226
|
+
required: true,
|
|
227
|
+
content: {
|
|
228
|
+
"application/json" => {
|
|
229
|
+
schema: object_schema(
|
|
230
|
+
{
|
|
231
|
+
email: {type: "string", description: "Email of the user"},
|
|
232
|
+
password: {type: "string", description: "Password of the user"},
|
|
233
|
+
callbackURL: {type: ["string", "null"], description: "Callback URL to use as a redirect for email verification"},
|
|
234
|
+
rememberMe: {type: ["boolean", "null"], default: true, description: "If this is false, the session will not be remembered. Default is `true`."}
|
|
235
|
+
},
|
|
236
|
+
required: ["email", "password"]
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
responses: {
|
|
242
|
+
"200" => {
|
|
243
|
+
description: "Success - Returns either session details or redirect URL",
|
|
244
|
+
content: {
|
|
245
|
+
"application/json" => {
|
|
246
|
+
schema: session_response_schema(description: "Session response when idToken is provided", nullable_url: true)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
when ["/sign-in/social", "POST"]
|
|
253
|
+
{
|
|
254
|
+
description: "Sign in with a social provider",
|
|
255
|
+
operationId: "socialSignIn",
|
|
256
|
+
requestBody: {
|
|
257
|
+
required: true,
|
|
258
|
+
content: {
|
|
259
|
+
"application/json" => {
|
|
260
|
+
schema: object_schema(
|
|
261
|
+
{
|
|
262
|
+
provider: {type: "string"},
|
|
263
|
+
callbackURL: {type: ["string", "null"], description: "Callback URL to redirect to after the user has signed in"},
|
|
264
|
+
errorCallbackURL: {type: ["string", "null"], description: "Callback URL to redirect to if an error happens"},
|
|
265
|
+
newUserCallbackURL: {type: ["string", "null"]},
|
|
266
|
+
disableRedirect: {type: ["boolean", "null"], description: "Disable automatic redirection to the provider. Useful for handling the redirection yourself"},
|
|
267
|
+
requestSignUp: {type: ["boolean", "null"], description: "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider"},
|
|
268
|
+
loginHint: {type: ["string", "null"], description: "The login hint to use for the authorization code request"},
|
|
269
|
+
additionalData: {type: ["string", "null"]},
|
|
270
|
+
scopes: {type: ["array", "null"], description: "Array of scopes to request from the provider. This will override the default scopes passed."},
|
|
271
|
+
idToken: {
|
|
272
|
+
type: ["object", "null"],
|
|
273
|
+
properties: {
|
|
274
|
+
token: {type: "string", description: "ID token from the provider"},
|
|
275
|
+
accessToken: {type: ["string", "null"], description: "Access token from the provider"},
|
|
276
|
+
refreshToken: {type: ["string", "null"], description: "Refresh token from the provider"},
|
|
277
|
+
expiresAt: {type: ["number", "null"], description: "Expiry date of the token"},
|
|
278
|
+
nonce: {type: ["string", "null"], description: "Nonce used to generate the token"}
|
|
279
|
+
},
|
|
280
|
+
required: ["token"]
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
required: ["provider"]
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
responses: {
|
|
289
|
+
"200" => {
|
|
290
|
+
description: "Success - Returns either session details or redirect URL",
|
|
291
|
+
content: {
|
|
292
|
+
"application/json" => {
|
|
293
|
+
schema: session_response_schema(description: "Session response when idToken is provided")
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
when ["/sign-up/email", "POST"]
|
|
300
|
+
{
|
|
301
|
+
description: "Sign up a user using email and password",
|
|
302
|
+
operationId: "signUpWithEmailAndPassword",
|
|
303
|
+
requestBody: {
|
|
304
|
+
content: {
|
|
305
|
+
"application/json" => {
|
|
306
|
+
schema: object_schema(
|
|
307
|
+
{
|
|
308
|
+
name: {type: "string", description: "The name of the user"},
|
|
309
|
+
email: {type: "string", description: "The email of the user"},
|
|
310
|
+
password: {type: "string", description: "The password of the user"},
|
|
311
|
+
image: {type: "string", description: "The profile image URL of the user"},
|
|
312
|
+
callbackURL: {type: "string", description: "The URL to use for email verification callback"},
|
|
313
|
+
rememberMe: {type: "boolean", description: "If this is false, the session will not be remembered. Default is `true`."}
|
|
314
|
+
},
|
|
315
|
+
required: ["name", "email", "password"]
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
responses: {
|
|
321
|
+
"200" => {
|
|
322
|
+
description: "Successfully created user",
|
|
323
|
+
content: {
|
|
324
|
+
"application/json" => {
|
|
325
|
+
schema: object_schema(
|
|
326
|
+
{
|
|
327
|
+
token: {type: "string", nullable: true, description: "Authentication token for the session"},
|
|
328
|
+
user: {type: "object", "$ref": "#/components/schemas/User"}
|
|
329
|
+
},
|
|
330
|
+
required: ["user"]
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
"422" => error_response("Unprocessable Entity. User already exists or failed to create user.")
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else
|
|
339
|
+
{}
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def object_schema(properties, required: [])
|
|
344
|
+
{
|
|
345
|
+
type: "object",
|
|
346
|
+
properties: properties,
|
|
347
|
+
required: required
|
|
348
|
+
}
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def session_response_schema(description:, nullable_url: false)
|
|
352
|
+
object_schema(
|
|
353
|
+
{
|
|
354
|
+
redirect: {type: "boolean", enum: [false]},
|
|
355
|
+
token: {type: "string", description: "Session token"},
|
|
356
|
+
url: nullable_url ? {type: "string", nullable: true} : {type: "string"},
|
|
357
|
+
user: {type: "object", "$ref": "#/components/schemas/User"}
|
|
358
|
+
},
|
|
359
|
+
required: ["redirect", "token", "user"]
|
|
360
|
+
).merge(description: description)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def open_api_user_response_schema
|
|
364
|
+
object_schema(
|
|
365
|
+
{
|
|
366
|
+
id: {type: "string", description: "The unique identifier of the user"},
|
|
367
|
+
email: {type: "string", format: "email", description: "The email address of the user"},
|
|
368
|
+
name: {type: "string", description: "The name of the user"},
|
|
369
|
+
image: {type: "string", format: "uri", nullable: true, description: "The profile image URL of the user"},
|
|
370
|
+
emailVerified: {type: "boolean", description: "Whether the email has been verified"},
|
|
371
|
+
createdAt: {type: "string", format: "date-time", description: "When the user was created"},
|
|
372
|
+
updatedAt: {type: "string", format: "date-time", description: "When the user was last updated"}
|
|
373
|
+
},
|
|
374
|
+
required: ["id", "email", "name", "emailVerified", "createdAt", "updatedAt"]
|
|
375
|
+
)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def empty_request_body
|
|
379
|
+
{
|
|
380
|
+
content: {
|
|
381
|
+
"application/json" => {
|
|
382
|
+
schema: {
|
|
383
|
+
type: "object",
|
|
384
|
+
properties: {}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def open_api_responses(responses = nil)
|
|
392
|
+
{"200" => success_response}.merge(default_error_responses).merge(responses || {})
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def success_response
|
|
396
|
+
{
|
|
397
|
+
description: "Success",
|
|
398
|
+
content: {
|
|
399
|
+
"application/json" => {
|
|
400
|
+
schema: {
|
|
401
|
+
type: "object",
|
|
402
|
+
properties: {}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def default_error_responses
|
|
410
|
+
{
|
|
411
|
+
"400" => error_response("Bad Request. Usually due to missing parameters, or invalid parameters.", required: true),
|
|
412
|
+
"401" => error_response("Unauthorized. Due to missing or invalid authentication.", required: true),
|
|
413
|
+
"403" => error_response("Forbidden. You do not have permission to access this resource or to perform this action."),
|
|
414
|
+
"404" => error_response("Not Found. The requested resource was not found."),
|
|
415
|
+
"429" => error_response("Too Many Requests. You have exceeded the rate limit. Try again later."),
|
|
416
|
+
"500" => error_response("Internal Server Error. This is a problem with the server that you cannot fix.")
|
|
417
|
+
}
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def error_response(description, required: false)
|
|
421
|
+
schema = {
|
|
422
|
+
type: "object",
|
|
423
|
+
properties: {
|
|
424
|
+
message: {
|
|
425
|
+
type: "string"
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
schema[:required] = ["message"] if required
|
|
430
|
+
{
|
|
431
|
+
description: description,
|
|
432
|
+
content: {
|
|
433
|
+
"application/json" => {
|
|
434
|
+
schema: schema
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def open_api_components(options)
|
|
441
|
+
Schema.auth_tables(options).each_with_object({}) do |(model, table), schemas|
|
|
442
|
+
name = model.to_s.split(/[_-]/).map(&:capitalize).join
|
|
443
|
+
schemas[name.to_sym] = schema_for_table(table)
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def schema_for_table(table)
|
|
448
|
+
required = []
|
|
449
|
+
properties = table[:fields].each_with_object({}) do |(field, attributes), result|
|
|
450
|
+
result[field.to_sym] = field_schema(attributes)
|
|
451
|
+
required << field if attributes[:required] && attributes[:input] != false && field != "id"
|
|
452
|
+
end
|
|
453
|
+
{type: "object", properties: properties, required: required}
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def field_schema(attributes)
|
|
457
|
+
type = case attributes[:type].to_s
|
|
458
|
+
when "date" then "string"
|
|
459
|
+
when "number" then "number"
|
|
460
|
+
when "boolean" then "boolean"
|
|
461
|
+
else "string"
|
|
462
|
+
end
|
|
463
|
+
schema = {type: type}
|
|
464
|
+
schema[:format] = "date-time" if attributes[:type].to_s == "date"
|
|
465
|
+
schema[:default] = attributes[:default_value].respond_to?(:call) ? "Generated at runtime" : attributes[:default_value] if attributes.key?(:default_value)
|
|
466
|
+
schema[:readOnly] = true if attributes[:input] == false
|
|
467
|
+
schema
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def open_api_html(schema, config)
|
|
471
|
+
nonce = config[:nonce] ? " nonce=\"#{config[:nonce]}\"" : ""
|
|
472
|
+
<<~HTML
|
|
473
|
+
<!doctype html>
|
|
474
|
+
<html>
|
|
475
|
+
<head>
|
|
476
|
+
<title>Scalar API Reference</title>
|
|
477
|
+
<meta charset="utf-8" />
|
|
478
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
479
|
+
</head>
|
|
480
|
+
<body>
|
|
481
|
+
<script id="api-reference" type="application/json">#{JSON.generate(schema)}</script>
|
|
482
|
+
<script#{nonce}>window.scalarTheme = "#{config[:theme]}";</script>
|
|
483
|
+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"#{nonce}></script>
|
|
484
|
+
</body>
|
|
485
|
+
</html>
|
|
486
|
+
HTML
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Plugins
|
|
5
|
+
module OrganizationSchema
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def build(config)
|
|
9
|
+
schema = {
|
|
10
|
+
organization: {
|
|
11
|
+
model_name: "organizations",
|
|
12
|
+
fields: {
|
|
13
|
+
name: {type: "string", required: true, sortable: true},
|
|
14
|
+
slug: {type: "string", required: true, unique: true, sortable: true, index: true},
|
|
15
|
+
logo: {type: "string", required: false},
|
|
16
|
+
metadata: {type: "string", required: false},
|
|
17
|
+
createdAt: {type: "date", required: true, default_value: -> { Time.now }},
|
|
18
|
+
updatedAt: {type: "date", required: false, on_update: -> { Time.now }}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
member: {
|
|
22
|
+
model_name: "members",
|
|
23
|
+
fields: {
|
|
24
|
+
organizationId: {type: "string", required: true, references: {model: "organization", field: "id"}, index: true},
|
|
25
|
+
userId: {type: "string", required: true, references: {model: "user", field: "id"}, index: true},
|
|
26
|
+
role: {type: "string", required: true, default_value: "member", sortable: true},
|
|
27
|
+
createdAt: {type: "date", required: true, default_value: -> { Time.now }}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
invitation: {
|
|
31
|
+
model_name: "invitations",
|
|
32
|
+
fields: {
|
|
33
|
+
organizationId: {type: "string", required: true, references: {model: "organization", field: "id"}, index: true},
|
|
34
|
+
email: {type: "string", required: true, sortable: true, index: true},
|
|
35
|
+
role: {type: "string", required: true, sortable: true},
|
|
36
|
+
status: {type: "string", required: true, sortable: true, default_value: "pending"},
|
|
37
|
+
expiresAt: {type: "date", required: true},
|
|
38
|
+
createdAt: {type: "date", required: true, default_value: -> { Time.now }},
|
|
39
|
+
inviterId: {type: "string", required: true, references: {model: "user", field: "id"}}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
session: {
|
|
43
|
+
fields: {
|
|
44
|
+
activeOrganizationId: {type: "string", required: false}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if truthy?(config.dig(:teams, :enabled))
|
|
50
|
+
schema[:team] = {
|
|
51
|
+
model_name: "teams",
|
|
52
|
+
fields: {
|
|
53
|
+
name: {type: "string", required: true},
|
|
54
|
+
organizationId: {type: "string", required: true, references: {model: "organization", field: "id"}, index: true},
|
|
55
|
+
createdAt: {type: "date", required: true, default_value: -> { Time.now }},
|
|
56
|
+
updatedAt: {type: "date", required: false, on_update: -> { Time.now }}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
schema[:teamMember] = {
|
|
60
|
+
model_name: "team_members",
|
|
61
|
+
fields: {
|
|
62
|
+
teamId: {type: "string", required: true, references: {model: "team", field: "id"}, index: true},
|
|
63
|
+
userId: {type: "string", required: true, references: {model: "user", field: "id"}, index: true},
|
|
64
|
+
createdAt: {type: "date", required: false, default_value: -> { Time.now }}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
schema[:invitation][:fields][:teamId] = {type: "string", required: false, sortable: true}
|
|
68
|
+
schema[:session][:fields][:activeTeamId] = {type: "string", required: false}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
if truthy?(config.dig(:dynamic_access_control, :enabled))
|
|
72
|
+
schema[:organizationRole] = {
|
|
73
|
+
model_name: "organization_roles",
|
|
74
|
+
fields: {
|
|
75
|
+
organizationId: {type: "string", required: true, references: {model: "organization", field: "id"}, index: true},
|
|
76
|
+
role: {type: "string", required: true, index: true},
|
|
77
|
+
permission: {type: "string", required: true},
|
|
78
|
+
createdAt: {type: "date", required: true, default_value: -> { Time.now }},
|
|
79
|
+
updatedAt: {type: "date", required: false, on_update: -> { Time.now }}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
merge_custom_schema(schema, config[:schema])
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def merge_custom_schema(base, custom)
|
|
88
|
+
return base unless custom.is_a?(Hash)
|
|
89
|
+
|
|
90
|
+
custom.each_with_object(base) do |(raw_model, raw_table), result|
|
|
91
|
+
model = Schema.storage_key(raw_model).to_sym
|
|
92
|
+
table = raw_table || {}
|
|
93
|
+
result[model] ||= {fields: {}}
|
|
94
|
+
result[model][:model_name] = table[:model_name] || table["modelName"] || table["model_name"] if table[:model_name] || table["modelName"] || table["model_name"]
|
|
95
|
+
fields = table[:fields] || table["fields"] || {}
|
|
96
|
+
additional = table[:additional_fields] || table["additionalFields"] || table["additional_fields"] || {}
|
|
97
|
+
result[model][:fields] = (result[model][:fields] || {}).merge(Plugins.storage_fields(fields)).merge(Plugins.storage_fields(additional))
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def truthy?(value)
|
|
102
|
+
value == true || value.to_s == "true"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|