veri 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02bcadafc5e2561e169ce65e920a7009f7549a0dfc1b5199cd471c01a8deac6f
4
- data.tar.gz: 9e8b74e83ea6882c5289f6109c9b4f2e8c492874f4ff9794315be7602410ea4a
3
+ metadata.gz: 85a6c779bd69de4d11430630c142f8e5e12e3574d7828f740abc19f0e51d291f
4
+ data.tar.gz: 0def50b534c6d728230ec7543fe6a007a518f9abe47e9c0d93aac45e7eb2f59f
5
5
  SHA512:
6
- metadata.gz: d7dedc00870041d1271c9c680226533030672546666bfac8a4fc0f5c7d26b4ff0eacd138b9edcb4418e017fd09e642e011b1dd06bb68ec69e2e3a4b96fc9805d
7
- data.tar.gz: 492b0c906f34d2f091030c1d3425d21fbc5442c18080107deafe1397079f8b060609be0fd224324987ff502f1f55dcf0a9be08434f387dca2579e148ce478966
6
+ metadata.gz: 714985726d7e5d04f55dbb1b94a73110a64ed1cb2853f565ddefda540cb13393969a1c4c3ec91e7de310822224dd844031d7d6b0cf57bd3b537c36da5bb6d55b
7
+ data.tar.gz: 7f2070d1f1da0260a8061b8abcff5a625245e4388f593d14a6edbdc8c8b496a3b55b5525bfda7a9abaae6bd91ffb218cb0d707d888f14114981c536d99a85e90
data/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## v0.4.0
2
+
3
+ ### Breaking
4
+
5
+ - Changed `veri_sessions` table to support multi-tenancy
6
+ - Renamed `revert_to_true_identity` session method to `to_true_identity`
7
+ - Session method `prune` no longer accepts a user argument
8
+ - Session method `terminate_all` no longer accepts a user argument
9
+
10
+ ### Features
11
+
12
+ - Added multi-tenancy support
13
+ - Added session scopes to fetch active, expired, and inactive sessions
14
+ - Added user scopes to fetch locked and unlocked users
15
+
16
+ ## v0.3.1
17
+
18
+ ### Misc
19
+
20
+ - Minor improvements and code cleanup
21
+ - Relaxed dependency versions
22
+
1
23
  ## v0.3.0
2
24
 
3
25
  ### Breaking
data/README.md CHANGED
@@ -13,6 +13,7 @@ Veri is a cookie-based authentication library for Ruby on Rails that provides es
13
13
  - Return path handling
14
14
  - User impersonation feature
15
15
  - Account lockout functionality
16
+ - Multi-tenancy support
16
17
 
17
18
  > ⚠️ **Development Notice**<br>
18
19
  > Veri is functional but in early development. Breaking changes may occur in minor releases until v1.0!
@@ -26,6 +27,7 @@ Veri is a cookie-based authentication library for Ruby on Rails that provides es
26
27
  - [Controller Integration](#controller-integration)
27
28
  - [Authentication Sessions](#authentication-sessions)
28
29
  - [Account Lockout](#account-lockout)
30
+ - [Multi-Tenancy](#multi-tenancy)
29
31
  - [View Helpers](#view-helpers)
30
32
  - [Testing](#testing)
31
33
 
@@ -84,10 +86,10 @@ Your user model is automatically extended with password management methods:
84
86
 
85
87
  ```rb
86
88
  # Set or update a password
87
- user.update_password("new_password")
89
+ user.update_password("password")
88
90
 
89
91
  # Verify a password
90
- user.verify_password("submitted_password")
92
+ user.verify_password("password")
91
93
  ```
92
94
 
93
95
  ## Controller Integration
@@ -135,14 +137,27 @@ class SessionsController < ApplicationController
135
137
  end
136
138
  ```
137
139
 
138
- Available methods:
140
+ Available controller methods:
139
141
 
140
- - `current_user` - Returns authenticated user or `nil`
141
- - `logged_in?` - Returns `true` if user is authenticated
142
- - `log_in(user)` - Authenticates user and creates session, returns `true` on success or `false` if account is locked
143
- - `log_out` - Terminates current session
144
- - `return_path` - Returns path user was accessing before authentication
145
- - `current_session` - Returns current authentication session
142
+ ```rb
143
+ # Returns authenticated user or nil
144
+ current_user
145
+
146
+ # Returns true if user is authenticated
147
+ logged_in?
148
+
149
+ # Authenticates user and creates session, returns true on success or false if account is locked
150
+ log_in(user)
151
+
152
+ # Terminates current session
153
+ log_out
154
+
155
+ # Returns path user was trying to access before authentication, if any
156
+ return_path
157
+
158
+ # Returns current authentication session
159
+ current_session
160
+ ```
146
161
 
147
162
  ### User Impersonation (Shapeshifting)
148
163
 
@@ -159,7 +174,7 @@ module Admin
159
174
 
160
175
  def destroy
161
176
  original_user = current_session.true_identity
162
- current_session.revert_to_true_identity
177
+ current_session.to_true_identity
163
178
  redirect_to admin_dashboard_path, notice: "Returned to #{original_user.name}"
164
179
  end
165
180
  end
@@ -168,14 +183,26 @@ end
168
183
 
169
184
  Available session methods:
170
185
 
171
- - `shapeshift(user)` - Assume another user's identity (maintains original identity)
172
- - `revert_to_true_identity` - Return to original identity
173
- - `shapeshifted?` - Returns true if currently shapeshifted
174
- - `true_identity` - Returns original user when shapeshifted, otherwise current user
186
+ ```rb
187
+ # Assume another user's identity (maintains original identity)
188
+ session.shapeshift(user)
189
+
190
+ # Return to original identity
191
+ session.to_true_identity
192
+
193
+ # Returns true if currently shapeshifted
194
+ session.shapeshifted?
195
+
196
+ # Returns original user when shapeshifted, otherwise current user
197
+ session.true_identity
198
+ ```
175
199
 
176
200
  Controller helper:
177
201
 
178
- - `shapeshifter?` - Returns true if the current session is shapeshifted
202
+ ```rb
203
+ # Returns true if the current session is shapeshifted
204
+ shapeshifter?
205
+ ```
179
206
 
180
207
  ### When unauthenticated
181
208
 
@@ -208,7 +235,7 @@ Veri stores authentication sessions in the database, providing session managemen
208
235
 
209
236
  ```rb
210
237
  # Get all sessions for a user
211
- user.veri_sessions
238
+ user.sessions
212
239
 
213
240
  # Get current session in controller
214
241
  current_session
@@ -219,6 +246,7 @@ current_session
219
246
  ```rb
220
247
  session.identity
221
248
  # => authenticated user
249
+
222
250
  session.info
223
251
  # => {
224
252
  # device: "Desktop",
@@ -232,9 +260,23 @@ session.info
232
260
  ### Session Status
233
261
 
234
262
  ```rb
235
- session.active? # Session is active (neither expired nor inactive)
236
- session.inactive? # Session exceeded inactivity timeout
237
- session.expired? # Session exceeded maximum lifetime
263
+ # Session is active (neither expired nor inactive)
264
+ session.active?
265
+
266
+ # Session exceeded inactivity timeout
267
+ session.inactive?
268
+
269
+ # Session exceeded maximum lifetime
270
+ session.expired?
271
+
272
+ # Fetch active sessions
273
+ Veri::Session.active
274
+
275
+ # Fetch inactive sessions
276
+ Veri::Session.inactive
277
+
278
+ # Fetch expired sessions
279
+ Veri::Session.expired
238
280
  ```
239
281
 
240
282
  ### Session Management
@@ -243,12 +285,11 @@ session.expired? # Session exceeded maximum lifetime
243
285
  # Terminate a specific session
244
286
  session.terminate
245
287
 
246
- # Terminate all sessions for a user
247
- Veri::Session.terminate_all(user)
288
+ # Terminate all sessions
289
+ Veri::Session.terminate_all
248
290
 
249
291
  # Clean up expired/inactive sessions
250
- Veri::Session.prune # All sessions
251
- Veri::Session.prune(user) # Specific user's sessions
292
+ Veri::Session.prune
252
293
  ```
253
294
 
254
295
  ## Account Lockout
@@ -264,9 +305,62 @@ user.unlock!
264
305
 
265
306
  # Check if account is locked
266
307
  user.locked?
308
+
309
+ # Fetch locked users
310
+ User.locked
311
+
312
+ # Fetch unlocked users
313
+ User.unlocked
267
314
  ```
268
315
 
269
- When an account is locked, users cannot log in. If they're already logged in, their sessions will be terminated and they'll be treated as unauthenticated users.
316
+ When an account is locked, the user cannot log in. If the user is already logged in, their sessions will be terminated, and they will be treated as an unauthenticated user.
317
+
318
+ ## Multi-Tenancy
319
+
320
+ Veri supports multi-tenancy, allowing you to isolate authentication sessions between different tenants (e.g., organizations, clients, or subdomains).
321
+
322
+ ### Setting Up Multi-Tenancy
323
+
324
+ To enable multi-tenancy, override `current_tenant` method:
325
+
326
+ ```rb
327
+ class ApplicationController < ActionController::Base
328
+ include Veri::Authentication
329
+
330
+ with_authentication
331
+
332
+ private
333
+
334
+ def current_tenant
335
+ # Option 1: String-based tenancy (e.g., subdomain)
336
+ request.subdomain
337
+
338
+ # Option 2: Model-based tenancy (e.g., organization)
339
+ # Company.find_by(subdomain: request.subdomain)
340
+ end
341
+ end
342
+ ```
343
+
344
+ ### Session Tenant Access
345
+
346
+ Sessions expose their tenant through `tenant` method:
347
+
348
+ ```rb
349
+ # Returns the tenant (string, model instance, or nil in single-tenant applications)
350
+ session.tenant
351
+ ```
352
+
353
+ ### Migration Helpers
354
+
355
+ Handle tenant changes when models are renamed or removed. These are irreversible data migrations.
356
+
357
+ ```rb
358
+ # Rename a tenant class (e.g., when you rename your Organization model to Company)
359
+ migrate_authentication_tenant!("Organization", "Company")
360
+
361
+ # Remove orphaned tenant data (e.g., when you delete the Organization model entirely)
362
+ delete_authentication_tenant!("Organization")
363
+ ```
270
364
 
271
365
  ## View Helpers
272
366
 
@@ -10,6 +10,7 @@ class AddVeriAuthentication < ActiveRecord::Migration[<%= ActiveRecord::Migratio
10
10
  t.datetime :expires_at, null: false
11
11
  t.belongs_to :authenticatable, null: false, foreign_key: { to_table: <%= table_name.to_sym.inspect %> }, index: true<%= ", type: :uuid" if options[:uuid] %>
12
12
  t.belongs_to :original_authenticatable, foreign_key: { to_table: <%= table_name.to_sym.inspect %> }, index: true<%= ", type: :uuid" if options[:uuid] %>
13
+ t.belongs_to :tenant, polymorphic: true, index: true<%= ", type: :uuid" if options[:uuid] %>
13
14
  t.datetime :shapeshifted_at
14
15
  t.datetime :last_seen_at, null: false
15
16
  t.string :ip_address
@@ -11,46 +11,42 @@ module Veri
11
11
  default: :argon2,
12
12
  reader: true,
13
13
  constructor: -> (value) do
14
- Veri::Inputs.process(
14
+ Veri::Inputs::HashingAlgorithm.new(
15
15
  value,
16
- as: :hashing_algorithm,
17
16
  error: Veri::ConfigurationError,
18
17
  message: "Invalid hashing algorithm `#{value.inspect}`, supported algorithms are: #{Veri::Configuration::HASHERS.keys.join(", ")}"
19
- )
18
+ ).process
20
19
  end
21
20
  setting :inactive_session_lifetime,
22
21
  default: nil,
23
22
  reader: true,
24
23
  constructor: -> (value) do
25
- Veri::Inputs.process(
24
+ Veri::Inputs::Duration.new(
26
25
  value,
27
- as: :duration,
28
26
  optional: true,
29
27
  error: Veri::ConfigurationError,
30
28
  message: "Invalid inactive session lifetime `#{value.inspect}`, expected an instance of ActiveSupport::Duration or nil"
31
- )
29
+ ).process
32
30
  end
33
31
  setting :total_session_lifetime,
34
32
  default: 14.days,
35
33
  reader: true,
36
34
  constructor: -> (value) do
37
- Veri::Inputs.process(
35
+ Veri::Inputs::Duration.new(
38
36
  value,
39
- as: :duration,
40
37
  error: Veri::ConfigurationError,
41
38
  message: "Invalid total session lifetime `#{value.inspect}`, expected an instance of ActiveSupport::Duration"
42
- )
39
+ ).process
43
40
  end
44
41
  setting :user_model_name,
45
42
  default: "User",
46
43
  reader: true,
47
44
  constructor: -> (value) do
48
- Veri::Inputs.process(
45
+ Veri::Inputs::NonEmptyString.new(
49
46
  value,
50
- as: :non_empty_string,
51
47
  error: Veri::ConfigurationError,
52
48
  message: "Invalid user model name `#{value.inspect}`, expected an ActiveRecord model name as a string"
53
- )
49
+ ).process
54
50
  end
55
51
 
56
52
  HASHERS = {
@@ -64,12 +60,11 @@ module Veri
64
60
  end
65
61
 
66
62
  def user_model
67
- Veri::Inputs.process(
63
+ Veri::Inputs::Model.new(
68
64
  user_model_name,
69
- as: :model,
70
65
  error: Veri::ConfigurationError,
71
66
  message: "Invalid user model name `#{user_model_name}`, expected an ActiveRecord model name as a string"
72
- )
67
+ ).process
73
68
  end
74
69
  end
75
70
  end
@@ -1,3 +1,5 @@
1
+ require "digest/sha2"
2
+
1
3
  module Veri
2
4
  module Authentication
3
5
  extend ActiveSupport::Concern
@@ -29,27 +31,27 @@ module Veri
29
31
  end
30
32
 
31
33
  def current_session
32
- token = cookies.encrypted[:veri_token]
33
- @current_session ||= token ? Session.find_by(hashed_token: Digest::SHA256.hexdigest(token)) : nil
34
+ token = cookies.encrypted["#{auth_cookie_prefix}_token"]
35
+ @current_session ||= token ? Session.find_by(hashed_token: Digest::SHA256.hexdigest(token), **resolved_tenant) : nil
34
36
  end
35
37
 
36
38
  def log_in(authenticatable)
37
- processed_authenticatable = Veri::Inputs.process(
39
+ processed_authenticatable = Veri::Inputs::Authenticatable.new(
38
40
  authenticatable,
39
- as: :authenticatable,
40
41
  message: "Expected an instance of #{Veri::Configuration.user_model_name}, got `#{authenticatable.inspect}`"
41
- )
42
+ ).process
42
43
 
43
44
  return false if processed_authenticatable.locked?
44
45
 
45
- token = Veri::Session.establish(processed_authenticatable, request)
46
- cookies.encrypted.permanent[:veri_token] = { value: token, httponly: true }
46
+ token = Veri::Session.establish(processed_authenticatable, request, resolved_tenant)
47
+
48
+ cookies.encrypted.permanent["#{auth_cookie_prefix}_token"] = { value: token, httponly: true }
47
49
  true
48
50
  end
49
51
 
50
52
  def log_out
51
53
  current_session&.terminate
52
- cookies.delete(:veri_token)
54
+ cookies.delete("#{auth_cookie_prefix}_token")
53
55
  end
54
56
 
55
57
  def logged_in?
@@ -57,7 +59,7 @@ module Veri
57
59
  end
58
60
 
59
61
  def return_path
60
- cookies.signed[:veri_return_path]
62
+ cookies.signed["#{auth_cookie_prefix}_return_path"]
61
63
  end
62
64
 
63
65
  def shapeshifter?
@@ -80,7 +82,7 @@ module Veri
80
82
 
81
83
  log_out
82
84
 
83
- cookies.signed[:veri_return_path] = { value: request.fullpath, expires: 15.minutes.from_now } if request.get? && request.format.html?
85
+ cookies.signed["#{auth_cookie_prefix}_return_path"] = { value: request.fullpath, expires: 15.minutes.from_now } if request.get? && request.format.html?
84
86
 
85
87
  when_unauthenticated
86
88
  end
@@ -88,5 +90,19 @@ module Veri
88
90
  def when_unauthenticated
89
91
  request.format.html? ? redirect_back(fallback_location: root_path) : head(:unauthorized)
90
92
  end
93
+
94
+ def current_tenant = nil
95
+
96
+ def resolved_tenant
97
+ @resolved_tenant ||= Veri::Inputs::Tenant.new(
98
+ current_tenant,
99
+ error: Veri::InvalidTenantError,
100
+ message: "Expected a string, an ActiveRecord model instance, or nil, got `#{current_tenant.inspect}`"
101
+ ).resolve
102
+ end
103
+
104
+ def auth_cookie_prefix
105
+ @auth_cookie_prefix ||= "auth_#{Digest::SHA2.hexdigest(Marshal.dump(resolved_tenant))[0..7]}"
106
+ end
91
107
  end
92
108
  end
@@ -0,0 +1,20 @@
1
+ module Veri
2
+ module MigrationHelpers
3
+ def migrate_authentication_tenant!(old_tenant, new_tenant)
4
+ sessions = Veri::Session.where(tenant_type: old_tenant.to_s)
5
+
6
+ raise Veri::InvalidArgumentError, "No sessions exist in tenant #{old_tenant.inspect}" unless sessions.exists?
7
+ raise Veri::InvalidArgumentError, "Cannot migrate tenant to #{new_tenant.inspect}: class does not exist" unless new_tenant.to_s.safe_constantize
8
+
9
+ sessions.update_all(tenant_type: new_tenant.to_s)
10
+ end
11
+
12
+ def delete_authentication_tenant!(tenant)
13
+ sessions = Veri::Session.where(tenant_type: tenant.to_s)
14
+
15
+ raise Veri::InvalidArgumentError, "No sessions exist in tenant #{tenant.inspect}" unless sessions.exists?
16
+
17
+ sessions.delete_all
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ module Veri
2
+ module Inputs
3
+ class Authenticatable < Veri::Inputs::Base
4
+ private
5
+
6
+ def type = -> { self.class::Instance(Veri::Configuration.user_model) }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,33 @@
1
+ require "dry-types"
2
+
3
+ module Veri
4
+ module Inputs
5
+ class Base
6
+ include Dry.Types()
7
+
8
+ def initialize(value, optional: false, error: Veri::InvalidArgumentError, message: nil)
9
+ @value = value
10
+ @optional = optional
11
+ @error = error
12
+ @message = message
13
+ end
14
+
15
+ def process
16
+ type_checker = @optional ? type.call.optional : type.call
17
+ type_checker[@value]
18
+ rescue Dry::Types::CoercionError
19
+ raise_error
20
+ end
21
+
22
+ private
23
+
24
+ def type
25
+ raise NotImplementedError
26
+ end
27
+
28
+ def raise_error
29
+ raise @error, @message
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ module Veri
2
+ module Inputs
3
+ class Duration < Veri::Inputs::Base
4
+ private
5
+
6
+ def type = -> { self.class::Instance(ActiveSupport::Duration) }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Veri
2
+ module Inputs
3
+ class HashingAlgorithm < Veri::Inputs::Base
4
+ private
5
+
6
+ def type = -> { self.class::Strict::Symbol.enum(:argon2, :bcrypt, :scrypt) }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Veri
2
+ module Inputs
3
+ class Model < Veri::Inputs::Base
4
+ private
5
+
6
+ def type = -> { self.class::Strict::Class.constructor { _1.try(:safe_constantize) }.constrained(lt: ActiveRecord::Base) }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Veri
2
+ module Inputs
3
+ class NonEmptyString < Veri::Inputs::Base
4
+ private
5
+
6
+ def type = -> { self.class::Strict::String.constrained(min_size: 1) }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,33 @@
1
+ module Veri
2
+ module Inputs
3
+ class Tenant < Base
4
+ def resolve
5
+ case tenant = process
6
+ when nil
7
+ { tenant_type: nil, tenant_id: nil }
8
+ when String
9
+ { tenant_type: tenant.to_s, tenant_id: nil }
10
+ when ActiveRecord::Base
11
+ raise_error unless tenant.persisted?
12
+ { tenant_type: tenant.class.to_s, tenant_id: tenant.public_send(tenant.class.primary_key) }
13
+ else
14
+ tenant
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def type
21
+ -> {
22
+ self.class::Strict::String.constrained(min_size: 1) |
23
+ self.class::Instance(ActiveRecord::Base) |
24
+ self.class::Hash.schema(
25
+ tenant_type: self.class::Strict::String | self.class::Nil,
26
+ tenant_id: self.class::Strict::String | self.class::Strict::Integer | self.class::Nil
27
+ ) |
28
+ self.class::Nil
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -7,17 +7,16 @@ module Veri
7
7
 
8
8
  @@included = name
9
9
 
10
- has_many :veri_sessions, class_name: "Veri::Session", foreign_key: :authenticatable_id, dependent: :destroy
10
+ has_many :sessions, class_name: "Veri::Session", foreign_key: :authenticatable_id, dependent: :destroy
11
+
12
+ scope :locked, -> { where(locked: true) }
13
+ scope :unlocked, -> { where(locked: false) }
11
14
  end
12
15
 
13
16
  def update_password(password)
14
17
  update!(
15
18
  hashed_password: hasher.create(
16
- Veri::Inputs.process(
17
- password,
18
- as: :non_empty_string,
19
- message: "Expected a non-empty string, got `#{password.inspect}`"
20
- )
19
+ Veri::Inputs::NonEmptyString.new(password, message: "Expected a non-empty string, got `#{password.inspect}`").process
21
20
  ),
22
21
  password_updated_at: Time.current
23
22
  )
@@ -25,11 +24,7 @@ module Veri
25
24
 
26
25
  def verify_password(password)
27
26
  hasher.verify(
28
- Veri::Inputs.process(
29
- password,
30
- as: :non_empty_string,
31
- message: "Expected a non-empty string, got `#{password.inspect}`"
32
- ),
27
+ Veri::Inputs::NonEmptyString.new(password, message: "Expected a non-empty string, got `#{password.inspect}`").process,
33
28
  hashed_password
34
29
  )
35
30
  end
@@ -4,10 +4,16 @@ module Veri
4
4
  class Session < ActiveRecord::Base
5
5
  self.table_name = "veri_sessions"
6
6
 
7
- # rubocop:disable Rails/ReflectionClassName
8
7
  belongs_to :authenticatable, class_name: Veri::Configuration.user_model_name
9
8
  belongs_to :original_authenticatable, class_name: Veri::Configuration.user_model_name, optional: true
10
- # rubocop:enable Rails/ReflectionClassName
9
+ belongs_to :tenant, polymorphic: true, optional: true
10
+
11
+ scope :active, -> { where.not(id: expired.select(:id)).where.not(id: inactive.select(:id)) }
12
+ scope :expired, -> { where(expires_at: ...Time.current) }
13
+ scope :inactive, -> do
14
+ inactive_session_lifetime = Veri::Configuration.inactive_session_lifetime
15
+ inactive_session_lifetime ? where(last_seen_at: ...(Time.current - inactive_session_lifetime)) : none
16
+ end
11
17
 
12
18
  def active? = !expired? && !inactive?
13
19
 
@@ -26,12 +32,10 @@ module Veri
26
32
  alias terminate delete
27
33
 
28
34
  def update_info(request)
29
- processed_request = Veri::Inputs.process(request, as: :request, error: Veri::Error)
30
-
31
35
  update!(
32
36
  last_seen_at: Time.current,
33
- ip_address: processed_request.remote_ip,
34
- user_agent: processed_request.user_agent
37
+ ip_address: request.remote_ip,
38
+ user_agent: request.user_agent
35
39
  )
36
40
  end
37
41
 
@@ -55,15 +59,14 @@ module Veri
55
59
  update!(
56
60
  shapeshifted_at: Time.current,
57
61
  original_authenticatable: authenticatable,
58
- authenticatable: Veri::Inputs.process(
62
+ authenticatable: Veri::Inputs::Authenticatable.new(
59
63
  user,
60
- as: :authenticatable,
61
64
  message: "Expected an instance of #{Veri::Configuration.user_model_name}, got `#{user.inspect}`"
62
- )
65
+ ).process
63
66
  )
64
67
  end
65
68
 
66
- def revert_to_true_identity
69
+ def to_true_identity
67
70
  update!(
68
71
  shapeshifted_at: nil,
69
72
  authenticatable: original_authenticatable,
@@ -71,53 +74,52 @@ module Veri
71
74
  )
72
75
  end
73
76
 
77
+ def tenant
78
+ return tenant_type if tenant_type.present? && tenant_id.blank?
79
+
80
+ record = super
81
+
82
+ raise ActiveRecord::RecordNotFound.new(nil, tenant_type, nil, tenant_id) if tenant_id.present? && !record
83
+
84
+ record
85
+ end
86
+
74
87
  class << self
75
- def establish(user, request)
88
+ def establish(user, request, resolved_tenant)
76
89
  token = SecureRandom.hex(32)
77
90
  expires_at = Time.current + Veri::Configuration.total_session_lifetime
78
91
 
79
92
  new(
80
93
  hashed_token: Digest::SHA256.hexdigest(token),
81
94
  expires_at:,
82
- authenticatable: Veri::Inputs.process(user, as: :authenticatable, error: Veri::Error)
83
- ).update_info(
84
- Veri::Inputs.process(request, as: :request, error: Veri::Error)
85
- )
95
+ authenticatable: user,
96
+ **resolved_tenant
97
+ ).update_info(request)
86
98
 
87
99
  token
88
100
  end
89
101
 
90
- def prune(user = nil)
91
- scope = if user
92
- where(
93
- authenticatable: Veri::Inputs.process(
94
- user,
95
- as: :authenticatable,
96
- optional: true,
97
- message: "Expected an instance of #{Veri::Configuration.user_model_name} or nil, got `#{user.inspect}`"
98
- )
99
- )
100
- else
101
- all
102
- end
103
-
104
- expired_scope = scope.where(expires_at: ...Time.current)
102
+ def prune
103
+ expired_scope = where(expires_at: ...Time.current)
105
104
 
106
105
  if Veri::Configuration.inactive_session_lifetime
107
106
  inactive_cutoff = Time.current - Veri::Configuration.inactive_session_lifetime
108
- expired_scope = expired_scope.or(scope.where(last_seen_at: ...inactive_cutoff))
107
+ expired_scope = expired_scope.or(where(last_seen_at: ...inactive_cutoff))
109
108
  end
110
109
 
111
110
  expired_scope.delete_all
112
- end
113
111
 
114
- def terminate_all(user)
115
- Veri::Inputs.process(
116
- user,
117
- as: :authenticatable,
118
- message: "Expected an instance of #{Veri::Configuration.user_model_name}, got `#{user.inspect}`"
119
- ).veri_sessions.delete_all
112
+ ids = where.not(tenant_id: nil).includes(:tenant).filter_map do |session|
113
+ session.tenant
114
+ nil
115
+ rescue ActiveRecord::RecordNotFound
116
+ session.id
117
+ end
118
+
119
+ where(id: ids).delete_all if ids.any?
120
120
  end
121
+
122
+ alias terminate_all delete_all
121
123
  end
122
124
  end
123
125
  end
data/lib/veri/railtie.rb CHANGED
@@ -4,9 +4,23 @@ module Veri
4
4
  class Railtie < Rails::Railtie
5
5
  initializer "veri.to_prepare" do |app|
6
6
  app.config.to_prepare do
7
+ if ActiveRecord::Base.connection.data_source_exists?("veri_sessions")
8
+ Veri::Session.where.not(tenant_id: nil).distinct.pluck(:tenant_type).each do |tenant_class|
9
+ tenant_class.constantize
10
+ rescue NameError => e
11
+ raise Veri::Error, "Tenant not found: class `#{e.name}` may have been renamed or deleted"
12
+ end
13
+ end
14
+
7
15
  user_model = Veri::Configuration.user_model
8
16
  user_model.include Veri::Authenticatable unless user_model < Veri::Authenticatable
9
17
  end
10
18
  end
19
+
20
+ initializer "veri.extend_migration_helpers" do
21
+ ActiveSupport.on_load :active_record do
22
+ ActiveRecord::Migration.include Veri::MigrationHelpers
23
+ end
24
+ end
11
25
  end
12
26
  end
data/lib/veri/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Veri
2
- VERSION = "0.3.0".freeze
2
+ VERSION = "0.4.0".freeze
3
3
  end
data/lib/veri.rb CHANGED
@@ -7,13 +7,20 @@ require_relative "veri/password/argon2"
7
7
  require_relative "veri/password/bcrypt"
8
8
  require_relative "veri/password/scrypt"
9
9
 
10
- require_relative "veri/inputs"
10
+ require_relative "veri/inputs/base"
11
+ require_relative "veri/inputs/authenticatable"
12
+ require_relative "veri/inputs/duration"
13
+ require_relative "veri/inputs/hashing_algorithm"
14
+ require_relative "veri/inputs/model"
15
+ require_relative "veri/inputs/non_empty_string"
16
+ require_relative "veri/inputs/tenant"
11
17
  require_relative "veri/configuration"
12
18
 
13
19
  module Veri
14
20
  class Error < StandardError; end
15
- class ConfigurationError < Veri::Error; end
16
21
  class InvalidArgumentError < Veri::Error; end
22
+ class ConfigurationError < Veri::InvalidArgumentError; end
23
+ class InvalidTenantError < Veri::InvalidArgumentError; end
17
24
 
18
25
  delegate :configure, to: Veri::Configuration
19
26
  module_function :configure
@@ -22,5 +29,6 @@ end
22
29
  require_relative "veri/models/session"
23
30
  require_relative "veri/controllers/concerns/authentication"
24
31
  require_relative "veri/models/concerns/authenticatable"
32
+ require_relative "veri/helpers/migration_helpers"
25
33
 
26
34
  require_relative "veri/railtie"
data/veri.gemspec CHANGED
@@ -21,8 +21,8 @@ Gem::Specification.new do |spec|
21
21
 
22
22
  spec.add_dependency "argon2", "~> 2.0"
23
23
  spec.add_dependency "bcrypt", "~> 3.0"
24
- spec.add_dependency "dry-configurable", "~> 1.3"
25
- spec.add_dependency "dry-types", "~> 1.8"
24
+ spec.add_dependency "dry-configurable", "~> 1.1"
25
+ spec.add_dependency "dry-types", "~> 1.7"
26
26
  spec.add_dependency "rails", ">= 7.1", "< 8.1"
27
27
  spec.add_dependency "scrypt", "~> 3.0"
28
28
  spec.add_dependency "user_agent_parser", "~> 2.0"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: veri
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - enjaku4
@@ -43,28 +43,28 @@ dependencies:
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '1.3'
46
+ version: '1.1'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '1.3'
53
+ version: '1.1'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: dry-types
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '1.8'
60
+ version: '1.7'
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '1.8'
67
+ version: '1.7'
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: rails
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -125,7 +125,14 @@ files:
125
125
  - lib/veri.rb
126
126
  - lib/veri/configuration.rb
127
127
  - lib/veri/controllers/concerns/authentication.rb
128
- - lib/veri/inputs.rb
128
+ - lib/veri/helpers/migration_helpers.rb
129
+ - lib/veri/inputs/authenticatable.rb
130
+ - lib/veri/inputs/base.rb
131
+ - lib/veri/inputs/duration.rb
132
+ - lib/veri/inputs/hashing_algorithm.rb
133
+ - lib/veri/inputs/model.rb
134
+ - lib/veri/inputs/non_empty_string.rb
135
+ - lib/veri/inputs/tenant.rb
129
136
  - lib/veri/models/concerns/authenticatable.rb
130
137
  - lib/veri/models/session.rb
131
138
  - lib/veri/password/argon2.rb
data/lib/veri/inputs.rb DELETED
@@ -1,31 +0,0 @@
1
- require "dry-types"
2
-
3
- module Veri
4
- module Inputs
5
- extend self
6
-
7
- include Dry.Types()
8
-
9
- TYPES = {
10
- hashing_algorithm: -> { self::Strict::Symbol.enum(:argon2, :bcrypt, :scrypt) },
11
- duration: -> { self::Instance(ActiveSupport::Duration) },
12
- non_empty_string: -> { self::Strict::String.constrained(min_size: 1) },
13
- model: -> { self::Strict::Class.constructor { _1.try(:safe_constantize) || _1 }.constrained(lt: ActiveRecord::Base) },
14
- authenticatable: -> { self::Instance(Veri::Configuration.user_model) },
15
- request: -> { self::Instance(ActionDispatch::Request) }
16
- }.freeze
17
-
18
- def process(value, as:, optional: false, error: Veri::InvalidArgumentError, message: nil)
19
- checker = type_for(as)
20
- checker = checker.optional if optional
21
-
22
- checker[value]
23
- rescue Dry::Types::CoercionError => e
24
- raise error, message || e.message
25
- end
26
-
27
- private
28
-
29
- def type_for(name) = Veri::Inputs::TYPES.fetch(name).call
30
- end
31
- end