omniauth-identity 3.1.3 → 3.1.5

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.
data/REEK ADDED
File without changes
data/RUBOCOP.md ADDED
@@ -0,0 +1,71 @@
1
+ # RuboCop Usage Guide
2
+
3
+ ## Overview
4
+
5
+ A tale of two RuboCop plugin gems.
6
+
7
+ ### RuboCop Gradual
8
+
9
+ This project uses `rubocop_gradual` instead of vanilla RuboCop for code style checking. The `rubocop_gradual` tool allows for gradual adoption of RuboCop rules by tracking violations in a lock file.
10
+
11
+ ### RuboCop LTS
12
+
13
+ This project uses `rubocop-lts` to ensure, on a best-effort basis, compatibility with Ruby >= 1.9.2.
14
+ RuboCop rules are meticulously configured by the `rubocop-lts` family of gems to ensure that a project is compatible with a specific version of Ruby. See: https://rubocop-lts.gitlab.io for more.
15
+
16
+ ## Checking RuboCop Violations
17
+
18
+ To check for RuboCop violations in this project, always use:
19
+
20
+ ```bash
21
+ bundle exec rake rubocop_gradual:check
22
+ ```
23
+
24
+ **Do not use** the standard RuboCop commands like:
25
+ - `bundle exec rubocop`
26
+ - `rubocop`
27
+
28
+ ## Understanding the Lock File
29
+
30
+ The `.rubocop_gradual.lock` file tracks all current RuboCop violations in the project. This allows the team to:
31
+
32
+ 1. Prevent new violations while gradually fixing existing ones
33
+ 2. Track progress on code style improvements
34
+ 3. Ensure CI builds don't fail due to pre-existing violations
35
+
36
+ ## Common Commands
37
+
38
+ - **Check violations**
39
+ - `bundle exec rake rubocop_gradual`
40
+ - `bundle exec rake rubocop_gradual:check`
41
+ - **(Safe) Autocorrect violations, and update lockfile if no new violations**
42
+ - `bundle exec rake rubocop_gradual:autocorrect`
43
+ - **Force update the lock file (w/o autocorrect) to match violations present in code**
44
+ - `bundle exec rake rubocop_gradual:force_update`
45
+
46
+ ## Workflow
47
+
48
+ 1. Before submitting a PR, run `bundle exec rake rubocop_gradual:autocorrect`
49
+ a. or just the default `bundle exec rake`, as autocorrection is a pre-requisite of the default task.
50
+ 2. If there are new violations, either:
51
+ - Fix them in your code
52
+ - Run `bundle exec rake rubocop_gradual:force_update` to update the lock file (only for violations you can't fix immediately)
53
+ 3. Commit the updated `.rubocop_gradual.lock` file along with your changes
54
+
55
+ ## Never add inline RuboCop disables
56
+
57
+ Do not add inline `rubocop:disable` / `rubocop:enable` comments anywhere in the codebase (including specs, except when following the few existing `rubocop:disable` patterns for a rule already being disabled elsewhere in the code). We handle exceptions in two supported ways:
58
+
59
+ - Permanent/structural exceptions: prefer adjusting the RuboCop configuration (e.g., in `.rubocop.yml`) to exclude a rule for a path or file pattern when it makes sense project-wide.
60
+ - Temporary exceptions while improving code: record the current violations in `.rubocop_gradual.lock` via the gradual workflow:
61
+ - `bundle exec rake rubocop_gradual:autocorrect` (preferred; will autocorrect what it can and update the lock only if no new violations were introduced)
62
+ - If needed, `bundle exec rake rubocop_gradual:force_update` (as a last resort when you cannot fix the newly reported violations immediately)
63
+
64
+ In general, treat the rules as guidance to follow; fix violations rather than ignore them. For example, RSpec conventions in this project expect `described_class` to be used in specs that target a specific class under test.
65
+
66
+ ## Benefits of rubocop_gradual
67
+
68
+ - Allows incremental adoption of code style rules
69
+ - Prevents CI failures due to pre-existing violations
70
+ - Provides a clear record of code style debt
71
+ - Enables focused efforts on improving code quality over time
data/SECURITY.md CHANGED
@@ -18,23 +18,11 @@ Tidelift will coordinate the fix and disclosure.
18
18
 
19
19
  ## Additional Support
20
20
 
21
- Interested in support for versions older than the latest release?
22
- Consider sponsoring the project / maintainer.
23
-
24
- [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate to my FLOSS or refugee efforts at ko-fi.com][🖇kofi-img]][🖇kofi] [![Donate to my FLOSS or refugee efforts using Patreon][🖇patreon-img]][🖇patreon]
25
-
26
- [⛳liberapay-img]: https://img.shields.io/liberapay/goal/pboling.svg?logo=liberapay
27
- [⛳liberapay]: https://liberapay.com/pboling/donate
28
- [🖇sponsor-img]: https://img.shields.io/badge/Sponsor_Me!-pboling.svg?style=social&logo=github
29
- [🖇sponsor]: https://github.com/sponsors/pboling
30
- [🖇polar-img]: https://img.shields.io/badge/polar-donate-yellow.svg
31
- [🖇polar]: https://polar.sh/pboling
32
- [🖇kofi-img]: https://img.shields.io/badge/a_more_different_coffee-✓-yellow.svg
33
- [🖇kofi]: https://ko-fi.com/O5O86SNP4
34
- [🖇patreon-img]: https://img.shields.io/badge/patreon-donate-yellow.svg
35
- [🖇patreon]: https://patreon.com/galtzo
36
- [🖇buyme]: https://www.buymeacoffee.com/pboling
37
- [🖇buyme-small-img]: https://img.shields.io/badge/buy_me_a_coffee-✓-yellow.svg?style=flat
21
+ If you are interested in support for versions older than the latest release,
22
+ please consider sponsoring the project / maintainer @ https://liberapay.com/pboling/donate,
23
+ or find other sponsorship links in the [README].
24
+
25
+ [README]: README.md
38
26
 
39
27
  ## Enterprise Support
40
28
 
@@ -42,4 +30,4 @@ Available as part of the Tidelift Subscription.
42
30
 
43
31
  The maintainers of this library and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers for the exact packages you use. [Learn more.][tidelift-ref]
44
32
 
45
- [tidelift-ref]: https://tidelift.com/subscription/pkg/rubygems-omniauth-identity?utm_source=rubygems-omniauth-identity&utm_medium=referral&utm_campaign=enterprise&utm_term=repo
33
+ [tidelift-ref]: https://tidelift.com/subscription/pkg/rubygems-omniauth-identity?utm_source=rubygems-omniauth-identity&utm_medium=referral&utm_campaign=enterprise&utm_term=repo
@@ -9,21 +9,36 @@ module OmniAuth
9
9
  # All methods marked as abstract must be implemented in the
10
10
  # including class for things to work properly.
11
11
  #
12
- ### Singleton API
12
+ # ### Singleton API
13
13
  #
14
14
  # * locate(key)
15
15
  # * create(*args) - Deprecated in v3.0.5; Will be removed in v4.0
16
16
  #
17
- ### Instance API
17
+ # ### Instance API
18
18
  #
19
19
  # * save
20
20
  # * persisted?
21
21
  # * authenticate(password)
22
22
  #
23
+ # @example Including the Model
24
+ # class User
25
+ # include OmniAuth::Identity::Model
26
+ # # Implement required methods...
27
+ # end
23
28
  module Model
29
+ # @!attribute [r] SCHEMA_ATTRIBUTES
30
+ # Standard OmniAuth schema attributes that may be stored in the model.
31
+ # @return [Array<String>] List of attribute names.
24
32
  SCHEMA_ATTRIBUTES = %w[name email nickname first_name last_name location description image phone].freeze
25
33
 
26
34
  class << self
35
+ # Called when this module is included in a model class.
36
+ #
37
+ # Extends the base class with ClassMethods and includes necessary APIs
38
+ # if they are not already defined.
39
+ #
40
+ # @param base [Class] the model class including this module
41
+ # @return [void]
27
42
  def included(base)
28
43
  base.extend(ClassMethods)
29
44
  base.extend(ClassCreateApi) unless base.respond_to?(:create)
@@ -33,12 +48,13 @@ module OmniAuth
33
48
  end
34
49
  end
35
50
 
51
+ # Class-level methods for OmniAuth Identity models.
36
52
  module ClassMethods
37
53
  # Authenticate a user with the given key and password.
38
54
  #
39
55
  # @param [String] conditions The unique login key provided for a given identity.
40
56
  # @param [String] password The presumed password for the identity.
41
- # @return [Model, false] An instance of the identity model class.
57
+ # @return [Model, false] An instance of the identity model class or false if authentication fails.
42
58
  def authenticate(conditions, password)
43
59
  instance = locate(conditions)
44
60
  return false unless instance
@@ -48,6 +64,8 @@ module OmniAuth
48
64
 
49
65
  # Used to set or retrieve the method that will be used to get
50
66
  # and set the user-supplied authentication key.
67
+ #
68
+ # @param method [String, Symbol, false] The method name to set, or false to retrieve.
51
69
  # @return [String] The method name.
52
70
  def auth_key(method = false)
53
71
  @auth_key = method.to_s unless method == false
@@ -58,7 +76,7 @@ module OmniAuth
58
76
 
59
77
  # Locate an identity given its unique login key.
60
78
  #
61
- # @abstract
79
+ # @abstract Subclasses must implement this method.
62
80
  # @param [String] _key The unique login key.
63
81
  # @return [Model] An instance of the identity model class.
64
82
  def locate(_key)
@@ -66,6 +84,7 @@ module OmniAuth
66
84
  end
67
85
  end
68
86
 
87
+ # Provides a create method for models that don't have one.
69
88
  module ClassCreateApi
70
89
  # Persists a new Identity object to the ORM.
71
90
  # Only included if the class doesn't define create, as a reminder to define create.
@@ -81,6 +100,7 @@ module OmniAuth
81
100
  end
82
101
  end
83
102
 
103
+ # Provides a save method for models that don't have one.
84
104
  module InstanceSaveApi
85
105
  # Persists a new Identity object to the ORM.
86
106
  # Default raises an error. Override as needed per ORM.
@@ -88,6 +108,7 @@ module OmniAuth
88
108
  # since it is a pattern many ORMs follow
89
109
  #
90
110
  # @abstract
111
+ # @param _options [Hash] Options for saving.
91
112
  # @return [Model] An instance of the identity model class.
92
113
  # @since 3.0.5
93
114
  def save(**_options, &_block)
@@ -95,12 +116,13 @@ module OmniAuth
95
116
  end
96
117
  end
97
118
 
119
+ # Provides a persisted? method for models that don't have one.
98
120
  module InstancePersistedApi
99
121
  # Checks if the Identity object is persisted in the ORM.
100
122
  # Default raises an error. Override as needed per ORM.
101
123
  #
102
124
  # @abstract
103
- # @return [true or false] true if object exists, false if not.
125
+ # @return [true, false] true if object exists, false if not.
104
126
  # @since 3.0.5
105
127
  def persisted?
106
128
  raise NotImplementedError
@@ -110,9 +132,9 @@ module OmniAuth
110
132
  # Returns self if the provided password is correct, false
111
133
  # otherwise.
112
134
  #
113
- # @abstract
135
+ # @abstract Subclasses must implement this method.
114
136
  # @param [String] _password The password to check.
115
- # @return [self or false] Self if authenticated, false if not.
137
+ # @return [self, false] Self if authenticated, false if not.
116
138
  def authenticate(_password)
117
139
  raise NotImplementedError
118
140
  end
@@ -6,8 +6,34 @@ module OmniAuth
6
6
  module Identity
7
7
  module Models
8
8
  # ActiveRecord is an ORM for MySQL, PostgreSQL, and SQLite3:
9
- # https://guides.rubyonrails.org/active_record_basics.html
10
- # NOTE: ActiveRecord is based on ActiveModel.
9
+ # https://guides.rubyonrails.org/active_record_basics.html
10
+ #
11
+ # This class provides a base for OmniAuth Identity models using ActiveRecord,
12
+ # including secure password handling and authentication key management.
13
+ #
14
+ # @example Usage
15
+ # class User < OmniAuth::Identity::Models::ActiveRecord
16
+ # # Add your fields here, e.g.:
17
+ # # self.table_name = 'users'
18
+ # # has_many :posts
19
+ # end
20
+ #
21
+ # # Migration example:
22
+ # # create_table :users do |t|
23
+ # # t.string :email, null: false
24
+ # # t.string :password_digest, null: false
25
+ # # t.timestamps
26
+ # # end
27
+ #
28
+ # user = User.new(email: 'user@example.com', password: 'password')
29
+ # user.save
30
+ #
31
+ # # Authenticate a user
32
+ # authenticated_user = User.locate(email: 'user@example.com')
33
+ # authenticated_user.authenticate('password') # => user or false
34
+ #
35
+ # @note ActiveRecord is based on ActiveModel, so validations are enabled by default.
36
+ # @note This is an abstract class; inherit from it to create your user model.
11
37
  class ActiveRecord < ::ActiveRecord::Base
12
38
  include ::OmniAuth::Identity::Model
13
39
  include ::OmniAuth::Identity::SecurePassword
@@ -16,11 +42,29 @@ module OmniAuth
16
42
  # validations: true (default) incurs a dependency on ActiveModel, but ActiveRecord is ActiveModel based.
17
43
  has_secure_password
18
44
 
45
+ # @!method self.auth_key=(key)
46
+ # Sets the authentication key for the model and adds uniqueness validation.
47
+ #
48
+ # @param key [Symbol, String] the attribute to use as the authentication key
49
+ # @return [void]
50
+ # @example
51
+ # class User < OmniAuth::Identity::Models::ActiveRecord
52
+ # self.auth_key = :email
53
+ # end
19
54
  def self.auth_key=(key)
20
55
  super
21
56
  validates_uniqueness_of(key, case_sensitive: false)
22
57
  end
23
58
 
59
+ # @!method self.locate(search_hash)
60
+ # Finds a record by the given search criteria.
61
+ #
62
+ # If the model has a 'provider' column, it defaults to 'identity'.
63
+ #
64
+ # @param search_hash [Hash] the attributes to search for
65
+ # @return [ActiveRecord::Base, nil] the first matching record or nil
66
+ # @example
67
+ # User.locate(email: 'user@example.com')
24
68
  def self.locate(search_hash)
25
69
  search_hash = search_hash.reverse_merge!("provider" => "identity") if column_names.include?("provider")
26
70
  where(search_hash).first
@@ -6,11 +6,39 @@ module OmniAuth
6
6
  module Identity
7
7
  module Models
8
8
  # CouchPotato is an ORM adapter for CouchDB:
9
- # https://github.com/langalex/couch_potato
10
- # NOTE: CouchPotato is based on ActiveModel.
11
- # NOTE: CouchPotato::Persistence must be included before OmniAuth::Identity::Models::CouchPotatoModule
12
- # NOTE: Includes "Module" in the name for invalid legacy reasons. Rename only with a major version bump.
9
+ # https://github.com/langalex/couch_potato
10
+ #
11
+ # This module provides OmniAuth Identity functionality for CouchPotato models,
12
+ # including secure password handling and authentication key management.
13
+ #
14
+ # @example Usage
15
+ # class User
16
+ # include CouchPotato::Persistence
17
+ #
18
+ # include OmniAuth::Identity::Models::CouchPotatoModule
19
+ #
20
+ # property :email
21
+ # property :password_digest
22
+ # end
23
+ #
24
+ # user = User.new(email: 'user@example.com', password: 'password')
25
+ # user.save
26
+ #
27
+ # # Authenticate a user
28
+ # authenticated_user = User.locate(email: 'user@example.com')
29
+ # authenticated_user.authenticate('password') # => user or false
30
+ #
31
+ # @note CouchPotato is based on ActiveModel, so validations are enabled by default.
32
+ # @note CouchPotato::Persistence must be included before OmniAuth::Identity::Models::CouchPotatoModule.
33
+ # @note Includes "Module" in the name for invalid legacy reasons. Rename only with a major version bump.
13
34
  module CouchPotatoModule
35
+ # Called when this module is included in a model class.
36
+ #
37
+ # This method extends the base class with OmniAuth Identity functionality,
38
+ # including secure password support and authentication key validation.
39
+ #
40
+ # @param base [Class] the model class including this module
41
+ # @return [void]
14
42
  def self.included(base)
15
43
  base.class_eval do
16
44
  include(::OmniAuth::Identity::Model)
@@ -19,15 +47,39 @@ module OmniAuth
19
47
  # validations: true (default) incurs a dependency on ActiveModel, but CouchPotato is ActiveModel based.
20
48
  has_secure_password
21
49
 
50
+ # @!method self.auth_key=(key)
51
+ # Sets the authentication key for the model and adds uniqueness validation.
52
+ #
53
+ # @param key [Symbol, String] the attribute to use as the authentication key
54
+ # @return [void]
55
+ # @example
56
+ # class User
57
+ # include OmniAuth::Identity::Models::CouchPotatoModule
58
+ # self.auth_key = :email
59
+ # end
22
60
  def self.auth_key=(key)
23
61
  super
24
62
  validates_uniqueness_of(key, case_sensitive: false)
25
63
  end
26
64
 
65
+ # @!method self.locate(search_hash)
66
+ # Finds a record by the given search criteria.
67
+ #
68
+ # @param search_hash [Hash] the attributes to search for
69
+ # @return [Object, nil] the first matching record or nil
70
+ # @example
71
+ # User.locate(email: 'user@example.com')
27
72
  def self.locate(search_hash)
28
73
  where(search_hash).first
29
74
  end
30
75
 
76
+ # @!method save
77
+ # Saves the document to the CouchDB database.
78
+ #
79
+ # @return [Boolean] true if saved successfully, false otherwise
80
+ # @example
81
+ # user = User.new(email: 'user@example.com', password: 'password')
82
+ # user.save # => true
31
83
  def save
32
84
  CouchPotato.database.save_document(self)
33
85
  end
@@ -6,9 +6,37 @@ module OmniAuth
6
6
  module Identity
7
7
  module Models
8
8
  # Mongoid is an ORM adapter for MongoDB:
9
- # https://github.com/mongodb/mongoid
10
- # NOTE: Mongoid is based on ActiveModel.
9
+ # https://github.com/mongodb/mongoid
10
+ #
11
+ # This module provides OmniAuth Identity functionality for Mongoid models,
12
+ # including secure password handling and authentication key management.
13
+ #
14
+ # @example Usage
15
+ # class User
16
+ # include Mongoid::Document
17
+ #
18
+ # include OmniAuth::Identity::Models::Mongoid
19
+ #
20
+ # field :email, type: String
21
+ # field :password_digest, type: String
22
+ # end
23
+ #
24
+ # user = User.new(email: 'user@example.com', password: 'password')
25
+ # user.save
26
+ #
27
+ # # Authenticate a user
28
+ # authenticated_user = User.locate(email: 'user@example.com')
29
+ # authenticated_user.authenticate('password') # => user or false
30
+ #
31
+ # @note Mongoid is based on ActiveModel, so validations are enabled by default.
11
32
  module Mongoid
33
+ # Called when this module is included in a model class.
34
+ #
35
+ # This method extends the base class with OmniAuth Identity functionality,
36
+ # including secure password support and authentication key validation.
37
+ #
38
+ # @param base [Class] the model class including this module
39
+ # @return [void]
12
40
  def self.included(base)
13
41
  base.class_eval do
14
42
  include(::OmniAuth::Identity::Model)
@@ -17,11 +45,28 @@ module OmniAuth
17
45
  # validations: true (default) incurs a dependency on ActiveModel, but Mongoid is ActiveModel based.
18
46
  has_secure_password
19
47
 
48
+ # @!method self.auth_key=(key)
49
+ # Sets the authentication key for the model and adds uniqueness validation.
50
+ #
51
+ # @param key [Symbol, String] the attribute to use as the authentication key
52
+ # @return [void]
53
+ # @example
54
+ # class User
55
+ # include OmniAuth::Identity::Models::Mongoid
56
+ # self.auth_key = :email
57
+ # end
20
58
  def self.auth_key=(key)
21
59
  super
22
60
  validates_uniqueness_of(key, case_sensitive: false)
23
61
  end
24
62
 
63
+ # @!method self.locate(search_hash)
64
+ # Finds a record by the given search criteria.
65
+ #
66
+ # @param search_hash [Hash] the attributes to search for
67
+ # @return [Mongoid::Document, nil] the first matching record or nil
68
+ # @example
69
+ # User.locate(email: 'user@example.com')
25
70
  def self.locate(search_hash)
26
71
  where(search_hash).first
27
72
  end
@@ -5,10 +5,37 @@ require "nobrainer"
5
5
  module OmniAuth
6
6
  module Identity
7
7
  module Models
8
- # NoBrainer is an ORM adapter for RethinkDB:
9
- # http://nobrainer.io/
10
- # NOTE: NoBrainer is based on ActiveModel.
8
+ # NoBrainer is an ORM adapter for RethinkDB: http://nobrainer.io/
9
+ #
10
+ # This module provides OmniAuth Identity functionality for NoBrainer models,
11
+ # including secure password handling and authentication key management.
12
+ #
13
+ # @example Usage
14
+ # class User
15
+ # include NoBrainer::Document
16
+ #
17
+ # include OmniAuth::Identity::Models::NoBrainer
18
+ #
19
+ # field :email
20
+ # field :password_digest
21
+ # end
22
+ #
23
+ # user = User.new(email: 'user@example.com', password: 'password')
24
+ # user.save
25
+ #
26
+ # # Authenticate a user
27
+ # authenticated_user = User.locate(email: 'user@example.com')
28
+ # authenticated_user.authenticate('password') # => user or false
29
+ #
30
+ # @note NoBrainer is based on ActiveModel, so validations are enabled by default.
11
31
  module NoBrainer
32
+ # Called when this module is included in a model class.
33
+ #
34
+ # This method extends the base class with OmniAuth Identity functionality,
35
+ # including secure password support and authentication key validation.
36
+ #
37
+ # @param base [Class] the model class including this module
38
+ # @return [void]
12
39
  def self.included(base)
13
40
  base.class_eval do
14
41
  include(::OmniAuth::Identity::Model)
@@ -17,11 +44,28 @@ module OmniAuth
17
44
  # validations: true (default) incurs a dependency on ActiveModel, but NoBrainer is ActiveModel based.
18
45
  has_secure_password
19
46
 
47
+ # @!method self.auth_key=(key)
48
+ # Sets the authentication key for the model and adds uniqueness validation.
49
+ #
50
+ # @param key [Symbol, String] the attribute to use as the authentication key
51
+ # @return [void]
52
+ # @example
53
+ # class User
54
+ # include OmniAuth::Identity::Models::NoBrainer
55
+ # self.auth_key = :email
56
+ # end
20
57
  def self.auth_key=(key)
21
58
  super
22
59
  validates_uniqueness_of(key, case_sensitive: false)
23
60
  end
24
61
 
62
+ # @!method self.locate(search_hash)
63
+ # Finds a record by the given search criteria.
64
+ #
65
+ # @param search_hash [Hash] the attributes to search for
66
+ # @return [Object, nil] the first matching record or nil
67
+ # @example
68
+ # User.locate(email: 'user@example.com')
25
69
  def self.locate(search_hash)
26
70
  where(search_hash).first
27
71
  end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bcrypt"
4
+
5
+ require "rom"
6
+ require "rom-sql"
7
+
8
+ module OmniAuth
9
+ module Identity
10
+ module Models
11
+ # ROM adapter for OmniAuth::Identity
12
+ # This provides a reusable adapter that makes ROM entities compatible with OmniAuth::Identity
13
+ #
14
+ # Usage:
15
+ # class Identity
16
+ # include OmniAuth::Identity::Models::Rom
17
+ #
18
+ # # Configure the ROM container and relation using the DSL (no `self.`):
19
+ # rom_container -> { MyDatabase.rom } # accepts a proc or a container object
20
+ # rom_relation_name :identities # optional, defaults to :identities
21
+ # owner_relation_name :owners # optional, for loading associated owner
22
+ # # Uses OmniAuth::Identity::Model.auth_key to set the auth key (defaults to :email)
23
+ # auth_key :email
24
+ # # Optional: override the password digest field name (defaults to :password_digest)
25
+ # password_field :password_digest
26
+ # end
27
+ module Rom
28
+ def self.included(base)
29
+ # Align with other adapters: rely on OmniAuth::Identity::Model for API
30
+ base.include(::OmniAuth::Identity::Model)
31
+ base.extend(ClassMethods)
32
+
33
+ base.class_eval do
34
+ # OmniAuth::Identity required instance API
35
+ # Authenticates the instance with the provided password
36
+ # Returns self on success, false otherwise (to match Model.authenticate contract)
37
+ def authenticate(password)
38
+ digest_key = self.class.password_field
39
+ password_digest = @identity_data[digest_key]
40
+ return false unless password_digest
41
+
42
+ begin
43
+ BCrypt::Password.new(password_digest) == password && self
44
+ rescue BCrypt::Errors::InvalidHash
45
+ false
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ module ClassMethods
52
+ # Default ROM relation name when none is configured
53
+ DEFAULT_RELATION_NAME = :identities
54
+
55
+ # Configuration DSL
56
+ # These methods act like the DSL on `OmniAuth::Identity::Model` (e.g. `auth_key`) —
57
+ # when called with an argument they set the configuration, and when called
58
+ # without an argument they return the current value (with sensible defaults).
59
+ def rom_container(value = false)
60
+ @rom_container = value unless value == false
61
+ container = @rom_container
62
+ container.respond_to?(:call) ? container.call : container
63
+ end
64
+
65
+ def rom_relation_name(value = false)
66
+ @rom_relation_name = value unless value == false
67
+ @rom_relation_name || DEFAULT_RELATION_NAME
68
+ end
69
+
70
+ def owner_relation_name(value = false)
71
+ @owner_relation_name = value unless value == false
72
+ @owner_relation_name
73
+ end
74
+
75
+ def password_field(value = false)
76
+ @password_field = value unless value == false
77
+ (@password_field || :password_digest).to_sym
78
+ end
79
+
80
+ # Align with other adapters: use Model.auth_key (getter/setter) for the login attribute
81
+ # Model.auth_key returns a String; convert to Symbol for ROM queries when needed.
82
+ def auth_key_symbol
83
+ (auth_key || "email").to_sym
84
+ end
85
+
86
+ # Locate an identity given conditions (Hash) or a raw key value.
87
+ # Mirrors other adapters by accepting a conditions hash.
88
+ # Returns an instance or nil.
89
+ def locate(conditions)
90
+ key_value = if conditions.is_a?(Hash)
91
+ conditions[auth_key_symbol] || conditions[auth_key.to_s]
92
+ else
93
+ conditions
94
+ end
95
+ return if key_value.nil?
96
+
97
+ relation = rom_container.relations[rom_relation_name]
98
+ identity_data = relation.where(auth_key_symbol => key_value).one
99
+ return unless identity_data
100
+
101
+ if owner_relation_name && identity_data[:owner_id]
102
+ owner_data = rom_container.relations[owner_relation_name].where(id: identity_data[:owner_id]).one
103
+ new(identity_data, owner_data)
104
+ else
105
+ new(identity_data)
106
+ end
107
+ end
108
+ end
109
+
110
+ # Instance shape matches other adapters for downstream usage
111
+ attr_reader :uid, :email, :name, :info, :owner
112
+
113
+ def initialize(identity_data, owner_data = nil)
114
+ @identity_data = identity_data
115
+ @owner_data = owner_data
116
+ @uid = identity_data[:id]
117
+ @email = identity_data[:email]
118
+
119
+ # Prefer owner name if available, else fall back to email
120
+ @name = if owner_data && owner_data[:name]
121
+ owner_data[:name]
122
+ else
123
+ identity_data[:email]
124
+ end
125
+
126
+ @info = {
127
+ "email" => @email,
128
+ "name" => @name,
129
+ }
130
+
131
+ @owner = owner_data
132
+ end
133
+
134
+ # Hash-like access to underlying ROM tuple
135
+ def [](key)
136
+ @identity_data[key]
137
+ end
138
+
139
+ # Convenience accessor for owner id
140
+ def owner_id
141
+ @identity_data[:owner_id]
142
+ end
143
+
144
+ # OmniAuth info hash
145
+ def to_hash
146
+ @info
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end