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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +106 -16
- data/lib/better_auth/adapters/base.rb +49 -0
- data/lib/better_auth/adapters/internal_adapter.rb +439 -0
- data/lib/better_auth/adapters/memory.rb +232 -0
- data/lib/better_auth/adapters/mongodb.rb +369 -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 +425 -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 +210 -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 +129 -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 +348 -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 +990 -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 +215 -0
- data/lib/better_auth/request_ip.rb +70 -0
- data/lib/better_auth/router.rb +365 -0
- data/lib/better_auth/routes/account.rb +211 -0
- data/lib/better_auth/routes/email_verification.rb +108 -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 +164 -0
- data/lib/better_auth/routes/session.rb +137 -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 +145 -0
- data/lib/better_auth/routes/social.rb +188 -0
- data/lib/better_auth/routes/user.rb +193 -0
- data/lib/better_auth/schema/sql.rb +191 -0
- data/lib/better_auth/schema.rb +275 -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 +55 -0
- data/lib/better_auth/social_providers/base.rb +67 -0
- data/lib/better_auth/social_providers/discord.rb +59 -0
- data/lib/better_auth/social_providers/github.rb +59 -0
- data/lib/better_auth/social_providers/gitlab.rb +54 -0
- data/lib/better_auth/social_providers/google.rb +65 -0
- data/lib/better_auth/social_providers/microsoft_entra_id.rb +65 -0
- data/lib/better_auth/social_providers.rb +9 -0
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +87 -2
- metadata +218 -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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a5e95c51ce68259fcccbbfabe1bd9ffa5383ce4c098161f1c2d54da76137fa0a
|
|
4
|
+
data.tar.gz: 7c81ade6292ced9b98c1c411c320e4ad6e05734ea891145f75a9205d6e4fdea0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4b7f5c635ce16aa7be7f3ea42c5fa03304b7c682821c8a82e55c81754ba330a88aba350f0a6ed59b171333766ba6a973c4a2d15cf3d46debeae42ddd59b602c5
|
|
7
|
+
data.tar.gz: 524ce3bfb3f023bcf057154fde0e88e0555753016d1297d67d40f2db6de6cf5516b0fcb8b513fd260cd284b38722eeffe9a02784d3d08a58cd5811cce23a1d39
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.1] - 2026-03-22
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Fixed gemspec files list to use `Dir.glob` instead of `git ls-files` for better CI compatibility
|
|
15
|
+
|
|
10
16
|
### Added
|
|
11
17
|
|
|
12
18
|
- Initial project setup
|
data/README.md
CHANGED
|
@@ -60,11 +60,94 @@ gem install better_auth
|
|
|
60
60
|
```ruby
|
|
61
61
|
require 'better_auth'
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
auth = BetterAuth.auth(
|
|
64
|
+
secret: ENV.fetch("BETTER_AUTH_SECRET"),
|
|
65
|
+
database: :memory
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Password Hashing
|
|
70
|
+
|
|
71
|
+
Better Auth Ruby uses upstream-compatible `scrypt` password hashes by default through Ruby's `OpenSSL::KDF.scrypt`, so no extra password-hashing gem is required for the default setup.
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
auth = BetterAuth.auth(
|
|
75
|
+
secret: ENV.fetch("BETTER_AUTH_SECRET"),
|
|
76
|
+
password_hasher: :scrypt # default
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Applications that prefer Ruby's familiar BCrypt ecosystem can opt in by adding `gem "bcrypt"` and configuring:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
auth = BetterAuth.auth(
|
|
84
|
+
secret: ENV.fetch("BETTER_AUTH_SECRET"),
|
|
85
|
+
password_hasher: :bcrypt
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Custom Better Auth-style password callbacks are still supported through `email_and_password[:password][:hash]` and `[:verify]`.
|
|
90
|
+
|
|
91
|
+
### Database Adapters
|
|
92
|
+
|
|
93
|
+
The core gem ships framework-agnostic adapters for memory, PostgreSQL, MySQL, SQLite, MongoDB, and MSSQL. Driver gems are loaded only when their adapter is instantiated.
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
auth = BetterAuth.auth(
|
|
97
|
+
secret: ENV.fetch("BETTER_AUTH_SECRET"),
|
|
98
|
+
database: BetterAuth::Adapters::SQLite.new(path: "storage/auth.sqlite3")
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
auth = BetterAuth.auth(
|
|
104
|
+
secret: ENV.fetch("BETTER_AUTH_SECRET"),
|
|
105
|
+
database: BetterAuth::Adapters::MongoDB.new(
|
|
106
|
+
database: mongo_client.database,
|
|
107
|
+
client: mongo_client,
|
|
108
|
+
transaction: false
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
auth = BetterAuth.auth(
|
|
115
|
+
secret: ENV.fetch("BETTER_AUTH_SECRET"),
|
|
116
|
+
database: BetterAuth::Adapters::MSSQL.new(url: ENV.fetch("DATABASE_URL"))
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Social Providers
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
require "better_auth"
|
|
124
|
+
|
|
125
|
+
auth = BetterAuth.auth(
|
|
126
|
+
secret: ENV.fetch("BETTER_AUTH_SECRET"),
|
|
127
|
+
social_providers: {
|
|
128
|
+
google: BetterAuth::SocialProviders.google(
|
|
129
|
+
client_id: ENV.fetch("GOOGLE_CLIENT_ID"),
|
|
130
|
+
client_secret: ENV.fetch("GOOGLE_CLIENT_SECRET")
|
|
131
|
+
),
|
|
132
|
+
github: BetterAuth::SocialProviders.github(
|
|
133
|
+
client_id: ENV.fetch("GITHUB_CLIENT_ID"),
|
|
134
|
+
client_secret: ENV.fetch("GITHUB_CLIENT_SECRET")
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### JavaScript Client
|
|
141
|
+
|
|
142
|
+
Ruby Better Auth exposes the same HTTP route surface. Frontend apps should use the upstream Better Auth JavaScript client and point it at the Ruby server:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { createAuthClient } from "better-auth/client";
|
|
146
|
+
|
|
147
|
+
export const authClient = createAuthClient({
|
|
148
|
+
baseURL: "http://localhost:3000",
|
|
149
|
+
basePath: "/api/auth",
|
|
150
|
+
});
|
|
68
151
|
```
|
|
69
152
|
|
|
70
153
|
### Rails Integration
|
|
@@ -97,6 +180,8 @@ end
|
|
|
97
180
|
|
|
98
181
|
## Development
|
|
99
182
|
|
|
183
|
+
Full documentation is being adapted in the root [`docs/`](/Users/sebastiansala/projects/better-auth/docs/README.md) app. Start with the Ruby-first installation, basic usage, Rack, Rails, PostgreSQL, and MySQL pages there; pages with a Ruby port warning still contain upstream TypeScript examples for reference.
|
|
184
|
+
|
|
100
185
|
### Quick Start
|
|
101
186
|
|
|
102
187
|
```bash
|
|
@@ -132,7 +217,7 @@ make test-coverage # Tests with coverage
|
|
|
132
217
|
make ci # Full CI (lint + test)
|
|
133
218
|
|
|
134
219
|
# Databases for testing
|
|
135
|
-
make db-up # Start PostgreSQL, MySQL, Redis
|
|
220
|
+
make db-up # Start PostgreSQL, MySQL, MongoDB, MSSQL, Redis
|
|
136
221
|
make db-down # Stop containers
|
|
137
222
|
```
|
|
138
223
|
|
|
@@ -183,10 +268,10 @@ git push origin feat/new-feature
|
|
|
183
268
|
|
|
184
269
|
**Automatic Release (GitHub Actions):**
|
|
185
270
|
|
|
186
|
-
Release is triggered
|
|
271
|
+
Release is triggered by package-prefixed tags so each gem can ship independently.
|
|
187
272
|
|
|
188
273
|
```bash
|
|
189
|
-
# STEP 1: Update
|
|
274
|
+
# STEP 1: Update the target package version file
|
|
190
275
|
# Example: VERSION = "0.1.1"
|
|
191
276
|
|
|
192
277
|
# STEP 2: Commit and push to main
|
|
@@ -194,19 +279,24 @@ git add lib/better_auth/version.rb
|
|
|
194
279
|
git commit -m "chore: bump version to 0.1.1"
|
|
195
280
|
git push origin main
|
|
196
281
|
|
|
197
|
-
# STEP 3:
|
|
282
|
+
# STEP 3: Create the tag for the gem you want to publish
|
|
283
|
+
git tag better_auth-v0.1.1
|
|
284
|
+
git push origin better_auth-v0.1.1
|
|
285
|
+
|
|
286
|
+
# STEP 4: GitHub Actions automatically:
|
|
198
287
|
# - Runs tests
|
|
199
288
|
# - Builds the gem
|
|
200
289
|
# - Publishes to RubyGems (if version is new)
|
|
201
|
-
# - Creates and pushes git tag (v0.1.1)
|
|
202
290
|
# - Creates GitHub Release
|
|
203
291
|
```
|
|
204
292
|
|
|
293
|
+
Use `better_auth-vX.Y.Z` for the core gem, `better_auth-rails-vX.Y.Z` for Rails, `better_auth-sinatra-vX.Y.Z` for Sinatra, and `better_auth-hanami-vX.Y.Z` for Hanami.
|
|
294
|
+
|
|
205
295
|
**Required GitHub Configuration:**
|
|
206
296
|
|
|
207
|
-
1.
|
|
208
|
-
2.
|
|
209
|
-
3. The workflow
|
|
297
|
+
1. In RubyGems, configure Trusted Publishing for each gem that should publish from CI.
|
|
298
|
+
2. Use this repository and workflow file: `.github/workflows/release.yml`.
|
|
299
|
+
3. The workflow exchanges GitHub's OIDC token for short-lived RubyGems credentials when the matching package tag is pushed.
|
|
210
300
|
|
|
211
301
|
**Dry-run options:**
|
|
212
302
|
|
|
@@ -231,8 +321,8 @@ gem build better_auth.gemspec
|
|
|
231
321
|
gem push better_auth-*.gem
|
|
232
322
|
|
|
233
323
|
# 4. Create and push the tag
|
|
234
|
-
git tag -a v0.1.1 -m "Release v0.1.1"
|
|
235
|
-
git push origin
|
|
324
|
+
git tag -a better_auth-v0.1.1 -m "Release better_auth v0.1.1"
|
|
325
|
+
git push origin better_auth-v0.1.1
|
|
236
326
|
```
|
|
237
327
|
|
|
238
328
|
### Project Structure
|
|
@@ -262,6 +352,6 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
262
352
|
|
|
263
353
|
## Security
|
|
264
354
|
|
|
265
|
-
If you discover a security vulnerability within Better Auth Ruby, please send an e-mail to [security@
|
|
355
|
+
If you discover a security vulnerability within Better Auth Ruby, please send an e-mail to [security@openparcel.dev](mailto:security@openparcel.dev).
|
|
266
356
|
|
|
267
357
|
All reports will be promptly addressed, and you'll be credited accordingly.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Adapters
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :options
|
|
7
|
+
|
|
8
|
+
def initialize(options)
|
|
9
|
+
@options = options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create(**)
|
|
13
|
+
raise NotImplementedError
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def find_one(**)
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def find_many(**)
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def update(**)
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def update_many(**)
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def delete(**)
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def delete_many(**)
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def count(**)
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def transaction
|
|
45
|
+
yield self
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module BetterAuth
|
|
8
|
+
module Adapters
|
|
9
|
+
class InternalAdapter
|
|
10
|
+
attr_reader :adapter, :options, :hooks
|
|
11
|
+
|
|
12
|
+
def initialize(adapter, options)
|
|
13
|
+
@adapter = adapter
|
|
14
|
+
@options = options
|
|
15
|
+
@hooks = DatabaseHooks.new(adapter, options)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create_oauth_user(user, account)
|
|
19
|
+
adapter.transaction do
|
|
20
|
+
created_user = create_user(user)
|
|
21
|
+
created_account = create_account(stringify_keys(account).merge("userId" => created_user["id"]))
|
|
22
|
+
{user: created_user, account: created_account}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def create_user(user)
|
|
27
|
+
data = timestamps.merge(stringify_keys(user))
|
|
28
|
+
data["email"] = data["email"].to_s.downcase if data["email"]
|
|
29
|
+
hooks.create(data, "user")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def create_account(account)
|
|
33
|
+
hooks.create(timestamps.merge(stringify_keys(account)), "account")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def link_account(account)
|
|
37
|
+
create_account(account)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def list_sessions(user_id)
|
|
41
|
+
if secondary_storage
|
|
42
|
+
active_session_entries(user_id).filter_map do |entry|
|
|
43
|
+
data = parse_storage(secondary_storage.get(entry.fetch("token")))
|
|
44
|
+
next unless data && data["session"]
|
|
45
|
+
|
|
46
|
+
normalize_session_dates(data["session"])
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
adapter.find_many(model: "session", where: [{field: "userId", value: user_id}])
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def list_users(limit: nil, offset: nil, sort_by: nil, where: nil)
|
|
54
|
+
adapter.find_many(model: "user", where: where || [], limit: limit, offset: offset, sort_by: sort_by)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def count_total_users(where: nil)
|
|
58
|
+
adapter.count(model: "user", where: where || [])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def delete_user(user_id)
|
|
62
|
+
delete_sessions(user_id) if !secondary_storage || options.session[:store_session_in_database]
|
|
63
|
+
hooks.delete_many([{field: "userId", value: user_id}], "account")
|
|
64
|
+
hooks.delete([{field: "id", value: user_id}], "user")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def create_session(user_id, dont_remember_me = false, override = nil, override_all = false, context = nil)
|
|
68
|
+
override = stringify_keys(override || {})
|
|
69
|
+
token = override.delete("token") || SecureRandom.hex(16)
|
|
70
|
+
base = {
|
|
71
|
+
"ipAddress" => "",
|
|
72
|
+
"userAgent" => "",
|
|
73
|
+
"expiresAt" => Time.now + (dont_remember_me ? 86_400 : options.session[:expires_in].to_i),
|
|
74
|
+
"userId" => user_id,
|
|
75
|
+
"token" => token
|
|
76
|
+
}.merge(timestamps)
|
|
77
|
+
data = override_all ? base.merge(override) : override.merge(base)
|
|
78
|
+
|
|
79
|
+
custom = secondary_storage && lambda do |session_data|
|
|
80
|
+
actual_session = apply_schema_create("session", session_data)
|
|
81
|
+
store_session(actual_session)
|
|
82
|
+
actual_session
|
|
83
|
+
end
|
|
84
|
+
execute_main = !secondary_storage || options.session[:store_session_in_database]
|
|
85
|
+
created = hooks.create(data, "session", custom: custom, context: context)
|
|
86
|
+
adapter.create(model: "session", data: data, force_allow_id: true) if secondary_storage && execute_main
|
|
87
|
+
created
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def find_session(token)
|
|
91
|
+
if secondary_storage
|
|
92
|
+
data = parse_storage(secondary_storage.get(token))
|
|
93
|
+
return nil unless data
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
session: normalize_session_dates(data["session"]),
|
|
97
|
+
user: normalize_user_dates(data["user"])
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
found = find_session_with_user(token)
|
|
102
|
+
return nil unless found && found["user"]
|
|
103
|
+
|
|
104
|
+
user = found.delete("user")
|
|
105
|
+
{session: found, user: user}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def find_sessions(tokens)
|
|
109
|
+
tokens.filter_map { |token| find_session(token) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def update_session(token, session)
|
|
113
|
+
data = stringify_keys(session)
|
|
114
|
+
if secondary_storage
|
|
115
|
+
return hooks.update(data, [{field: "token", value: token}], "session", custom: lambda { |actual_data|
|
|
116
|
+
update_stored_session(token, actual_data)
|
|
117
|
+
})
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
hooks.update(data, [{field: "token", value: token}], "session")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def delete_session(token)
|
|
124
|
+
if secondary_storage
|
|
125
|
+
data = parse_storage(secondary_storage.get(token))
|
|
126
|
+
if data && data["session"]
|
|
127
|
+
user_id = data["session"]["userId"]
|
|
128
|
+
entries = active_session_entries(user_id).reject { |entry| entry["token"] == token }
|
|
129
|
+
write_active_sessions(user_id, entries)
|
|
130
|
+
end
|
|
131
|
+
secondary_storage.delete(token)
|
|
132
|
+
return if !options.session[:store_session_in_database] || options.session[:preserve_session_in_database]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
hooks.delete([{field: "token", value: token}], "session")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def delete_sessions(user_id_or_tokens)
|
|
139
|
+
if secondary_storage
|
|
140
|
+
if user_id_or_tokens.is_a?(Array)
|
|
141
|
+
user_id_or_tokens.each { |token| secondary_storage.delete(token) }
|
|
142
|
+
else
|
|
143
|
+
active_session_entries(user_id_or_tokens).each { |entry| secondary_storage.delete(entry["token"]) }
|
|
144
|
+
secondary_storage.delete(active_key(user_id_or_tokens))
|
|
145
|
+
end
|
|
146
|
+
return if !options.session[:store_session_in_database] || options.session[:preserve_session_in_database]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
field = user_id_or_tokens.is_a?(Array) ? "token" : "userId"
|
|
150
|
+
operator = user_id_or_tokens.is_a?(Array) ? "in" : nil
|
|
151
|
+
hooks.delete_many([{field: field, value: user_id_or_tokens, operator: operator}], "session")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def delete_accounts(user_id)
|
|
155
|
+
hooks.delete_many([{field: "userId", value: user_id}], "account")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def delete_account(account_id)
|
|
159
|
+
hooks.delete([{field: "id", value: account_id}], "account")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def find_oauth_user(email, account_id, provider_id)
|
|
163
|
+
account = find_account_with_user(account_id, provider_id)
|
|
164
|
+
if account
|
|
165
|
+
user = account["user"] || adapter.find_one(model: "user", where: [{field: "email", value: email.to_s.downcase}])
|
|
166
|
+
return nil unless user
|
|
167
|
+
|
|
168
|
+
linked = account.dup
|
|
169
|
+
linked.delete("user")
|
|
170
|
+
return {user: user, linked_account: linked, accounts: [linked]}
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
found_user = adapter.find_one(model: "user", where: [{field: "email", value: email.to_s.downcase}])
|
|
174
|
+
return nil unless found_user
|
|
175
|
+
|
|
176
|
+
{user: found_user, linked_account: nil, accounts: find_accounts(found_user["id"])}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def find_user_by_email(email, include_accounts: false)
|
|
180
|
+
user = adapter.find_one(model: "user", where: [{field: "email", value: email.to_s.downcase}])
|
|
181
|
+
return nil unless user
|
|
182
|
+
|
|
183
|
+
{user: user, accounts: include_accounts ? find_accounts(user["id"]) : []}
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def find_user_by_id(user_id)
|
|
187
|
+
return nil if user_id.to_s.empty?
|
|
188
|
+
|
|
189
|
+
adapter.find_one(model: "user", where: [{field: "id", value: user_id}])
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def update_user(user_id, data)
|
|
193
|
+
user = hooks.update(stringify_keys(data), [{field: "id", value: user_id}], "user")
|
|
194
|
+
refresh_user_sessions(user) if user
|
|
195
|
+
user
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def update_user_by_email(email, data)
|
|
199
|
+
user = hooks.update(stringify_keys(data), [{field: "email", value: email.to_s.downcase}], "user")
|
|
200
|
+
refresh_user_sessions(user) if user
|
|
201
|
+
user
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def update_password(user_id, password)
|
|
205
|
+
hooks.update_many({password: password}, [{field: "userId", value: user_id}, {field: "providerId", value: "credential"}], "account")
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def find_accounts(user_id)
|
|
209
|
+
adapter.find_many(model: "account", where: [{field: "userId", value: user_id}])
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def find_account(account_id)
|
|
213
|
+
adapter.find_one(model: "account", where: [{field: "accountId", value: account_id}])
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def find_account_by_provider_id(account_id, provider_id)
|
|
217
|
+
adapter.find_one(model: "account", where: [{field: "accountId", value: account_id}, {field: "providerId", value: provider_id}])
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def find_account_by_user_id(user_id)
|
|
221
|
+
find_accounts(user_id)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def update_account(id, data)
|
|
225
|
+
hooks.update(stringify_keys(data), [{field: "id", value: id}], "account")
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def create_verification_value(data)
|
|
229
|
+
hooks.create(timestamps.merge(stringify_keys(data)), "verification")
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def find_verification_value(identifier)
|
|
233
|
+
values = adapter.find_many(
|
|
234
|
+
model: "verification",
|
|
235
|
+
where: [{field: "identifier", value: identifier}],
|
|
236
|
+
sort_by: {field: "createdAt", direction: "desc"},
|
|
237
|
+
limit: 1
|
|
238
|
+
)
|
|
239
|
+
hooks.delete_many([{field: "expiresAt", value: Time.now, operator: "lt"}], "verification") unless options.verification[:disable_cleanup]
|
|
240
|
+
values.first
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def delete_verification_value(id)
|
|
244
|
+
hooks.delete([{field: "id", value: id}], "verification")
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def delete_verification_by_identifier(identifier)
|
|
248
|
+
hooks.delete([{field: "identifier", value: identifier}], "verification")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def update_verification_value(id, data)
|
|
252
|
+
hooks.update(stringify_keys(data), [{field: "id", value: id}], "verification")
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
private
|
|
256
|
+
|
|
257
|
+
def secondary_storage
|
|
258
|
+
options.secondary_storage
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def joins_enabled?
|
|
262
|
+
!!options.experimental[:joins]
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def find_session_with_user(token)
|
|
266
|
+
return adapter.find_one(model: "session", where: [{field: "token", value: token}], join: {user: true}) if joins_enabled?
|
|
267
|
+
|
|
268
|
+
session = adapter.find_one(model: "session", where: [{field: "token", value: token}])
|
|
269
|
+
user = session && adapter.find_one(model: "user", where: [{field: "id", value: session["userId"]}])
|
|
270
|
+
(session && user) ? session.merge("user" => user) : nil
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def find_account_with_user(account_id, provider_id)
|
|
274
|
+
if joins_enabled?
|
|
275
|
+
return adapter.find_one(model: "account", where: [{field: "accountId", value: account_id}, {field: "providerId", value: provider_id}], join: {user: true})
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
account = adapter.find_one(model: "account", where: [{field: "accountId", value: account_id}, {field: "providerId", value: provider_id}])
|
|
279
|
+
user = account && adapter.find_one(model: "user", where: [{field: "id", value: account["userId"]}])
|
|
280
|
+
(account && user) ? account.merge("user" => user) : account
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def timestamps
|
|
284
|
+
now = Time.now
|
|
285
|
+
{"createdAt" => now, "updatedAt" => now}
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def stringify_keys(data)
|
|
289
|
+
data.each_with_object({}) do |(key, value), result|
|
|
290
|
+
result[Schema.storage_key(key)] = value
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def apply_schema_create(model, data)
|
|
295
|
+
fields = Schema.auth_tables(options)[model]&.fetch(:fields)
|
|
296
|
+
fields ||= session_additional_fields if model == "session"
|
|
297
|
+
output = stringify_keys(data)
|
|
298
|
+
fields.each do |field, attributes|
|
|
299
|
+
unless output.key?(field)
|
|
300
|
+
if attributes.key?(:default_value)
|
|
301
|
+
output[field] = resolve_default(attributes[:default_value])
|
|
302
|
+
elsif attributes[:required] && field != "id"
|
|
303
|
+
raise APIError.new("BAD_REQUEST", message: "#{field} is required")
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
output[field] = coerce_value(output[field], attributes) if output.key?(field)
|
|
307
|
+
end
|
|
308
|
+
output
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def session_additional_fields
|
|
312
|
+
(options.session[:additional_fields] || {}).each_with_object({}) do |(key, value), result|
|
|
313
|
+
result[Schema.storage_key(key)] = value
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def resolve_default(default)
|
|
318
|
+
default.respond_to?(:call) ? default.call : default
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def coerce_value(value, attributes)
|
|
322
|
+
return value if value.nil?
|
|
323
|
+
return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)
|
|
324
|
+
|
|
325
|
+
value
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def store_session(session)
|
|
329
|
+
user = adapter.find_one(model: "user", where: [{field: "id", value: session["userId"]}])
|
|
330
|
+
now_ms = current_millis
|
|
331
|
+
expires_ms = millis(session["expiresAt"])
|
|
332
|
+
entries = active_session_entries(session["userId"])
|
|
333
|
+
.reject { |entry| entry["expiresAt"].to_i <= now_ms || entry["token"] == session["token"] }
|
|
334
|
+
.push({"token" => session["token"], "expiresAt" => expires_ms})
|
|
335
|
+
.sort_by { |entry| entry["expiresAt"] }
|
|
336
|
+
write_active_sessions(session["userId"], entries)
|
|
337
|
+
secondary_storage.set(session["token"], JSON.generate({session: session, user: user}), ttl(expires_ms))
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def update_stored_session(token, data)
|
|
341
|
+
parsed = parse_storage(secondary_storage.get(token))
|
|
342
|
+
return nil unless parsed && parsed["session"]
|
|
343
|
+
|
|
344
|
+
merged = parsed["session"].merge(data)
|
|
345
|
+
merged["expiresAt"] = normalize_time(merged["expiresAt"])
|
|
346
|
+
merged["createdAt"] = normalize_time(merged["createdAt"])
|
|
347
|
+
merged["updatedAt"] = normalize_time(merged["updatedAt"])
|
|
348
|
+
secondary_storage.set(token, JSON.generate({session: merged, user: parsed["user"]}), ttl(millis(merged["expiresAt"])))
|
|
349
|
+
entries = active_session_entries(merged["userId"])
|
|
350
|
+
.reject { |entry| entry["token"] == token || entry["expiresAt"].to_i <= current_millis }
|
|
351
|
+
.push({"token" => token, "expiresAt" => millis(merged["expiresAt"])})
|
|
352
|
+
.sort_by { |entry| entry["expiresAt"] }
|
|
353
|
+
write_active_sessions(merged["userId"], entries)
|
|
354
|
+
merged
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def refresh_user_sessions(user)
|
|
358
|
+
return unless secondary_storage && user
|
|
359
|
+
|
|
360
|
+
active_session_entries(user["id"]).each do |entry|
|
|
361
|
+
parsed = parse_storage(secondary_storage.get(entry["token"]))
|
|
362
|
+
next unless parsed && parsed["session"]
|
|
363
|
+
|
|
364
|
+
secondary_storage.set(entry["token"], JSON.generate({session: parsed["session"], user: user}), ttl(millis(parsed["session"]["expiresAt"])))
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def active_session_entries(user_id)
|
|
369
|
+
raw = secondary_storage.get(active_key(user_id))
|
|
370
|
+
Array(parse_storage(raw)).map do |entry|
|
|
371
|
+
entry.transform_keys(&:to_s)
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def write_active_sessions(user_id, entries)
|
|
376
|
+
future = entries.select { |entry| entry["expiresAt"].to_i > current_millis }.sort_by { |entry| entry["expiresAt"] }
|
|
377
|
+
if future.empty?
|
|
378
|
+
secondary_storage.delete(active_key(user_id))
|
|
379
|
+
else
|
|
380
|
+
secondary_storage.set(active_key(user_id), JSON.generate(future), ttl(future.last["expiresAt"]))
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def active_key(user_id)
|
|
385
|
+
"active-sessions-#{user_id}"
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def parse_storage(value)
|
|
389
|
+
return value.transform_keys(&:to_s) if value.is_a?(Hash)
|
|
390
|
+
return value.map { |entry| entry.is_a?(Hash) ? entry.transform_keys(&:to_s) : entry } if value.is_a?(Array)
|
|
391
|
+
return nil unless value
|
|
392
|
+
|
|
393
|
+
parsed = JSON.parse(value)
|
|
394
|
+
parse_storage(parsed)
|
|
395
|
+
rescue JSON::ParserError
|
|
396
|
+
nil
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def normalize_session_dates(session)
|
|
400
|
+
return nil unless session
|
|
401
|
+
|
|
402
|
+
session.transform_keys(&:to_s).merge(
|
|
403
|
+
"expiresAt" => normalize_time(session["expiresAt"] || session[:expiresAt]),
|
|
404
|
+
"createdAt" => normalize_time(session["createdAt"] || session[:createdAt]),
|
|
405
|
+
"updatedAt" => normalize_time(session["updatedAt"] || session[:updatedAt])
|
|
406
|
+
)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def normalize_user_dates(user)
|
|
410
|
+
return nil unless user
|
|
411
|
+
|
|
412
|
+
user.transform_keys(&:to_s).merge(
|
|
413
|
+
"createdAt" => normalize_time(user["createdAt"] || user[:createdAt]),
|
|
414
|
+
"updatedAt" => normalize_time(user["updatedAt"] || user[:updatedAt])
|
|
415
|
+
)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def normalize_time(value)
|
|
419
|
+
return value if value.is_a?(Time)
|
|
420
|
+
return Time.at(value / 1000.0) if value.is_a?(Integer) && value > 10_000_000_000
|
|
421
|
+
return Time.at(value) if value.is_a?(Integer)
|
|
422
|
+
|
|
423
|
+
Time.parse(value.to_s)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def millis(value)
|
|
427
|
+
(normalize_time(value).to_f * 1000).to_i
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def ttl(expires_ms)
|
|
431
|
+
[(expires_ms - current_millis) / 1000, 0].max.floor
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def current_millis
|
|
435
|
+
(Time.now.to_f * 1000).to_i
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|