Class: Clavis::Generators::UserMethodGenerator

Inherits:
Rails::Generators::Base
  • Object
show all
Defined in:
lib/generators/clavis/user_method/user_method_generator.rb

Instance Method Summary collapse

Instance Method Details

#create_concernObject



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/generators/clavis/user_method/user_method_generator.rb', line 12

def create_concern
  # Create directory structure if it doesn't exist
  directory_path = "app/models/concerns"
  FileUtils.mkdir_p(directory_path) unless File.directory?(directory_path)

  # Create the concern file
  create_file "app/models/concerns/clavis_user_methods.rb", <<~RUBY
    # frozen_string_literal: true

    # This concern provides methods for finding or creating users from OAuth data
    # It is intended to be included in your User model
    module ClavisUserMethods
      extend ActiveSupport::Concern
    #{"  "}
      # Include the OauthAuthenticatable module to get helper methods
      included do
        include Clavis::Models::OauthAuthenticatable if defined?(Clavis::Models::OauthAuthenticatable)
    #{"    "}
        # Add a temporary attribute to track OAuth authentication during user creation
        # This can be used to conditionally skip password validation for OAuth users
        attr_accessor :skip_password_validation
    #{"    "}
        # IMPORTANT: If your User model uses has_secure_password, you need to handle
        # password validation. Uncomment and modify ONE of these approaches:
        #
        # APPROACH 1: Skip password validation for OAuth users (recommended)
        # validates :password, presence: true, length: { minimum: 8 },
        #           unless: -> { skip_password_validation }, on: :create
        #
        # APPROACH 2: Set a random secure password for OAuth users
        # before_validation :set_random_password, if: -> { skip_password_validation && respond_to?(:password=) }
        #
        # APPROACH 3: Use validate: false for OAuth users (less recommended)
        # See the #find_or_create_from_clavis method below
    #{"    "}
        # For approach 2, add this method
        # def set_random_password
        #   self.password = SecureRandom.hex(16)
        #   self.password_confirmation = password if respond_to?(:password_confirmation=)
        # end
      end

      class_methods do
        # Find or create a user from OAuth authentication
        # This method is called by Clavis when authenticating via OAuth
        def find_or_create_from_clavis(auth_hash)
          # First try to find an existing identity
          # For OpenID Connect providers like Google, we use the sub claim as the identifier
          # For other providers, we use the uid
          identity = if auth_hash[:id_token_claims]&.dig(:sub)
                      Clavis::OauthIdentity.find_by(
                        provider: auth_hash[:provider],
                        uid: auth_hash[:id_token_claims][:sub]
                      )
                    else
                      Clavis::OauthIdentity.find_by(
                        provider: auth_hash[:provider],
                        uid: auth_hash[:uid]
                      )
                    end
          return identity.user if identity&.user

          # Extract email from auth_hash (try various possible locations)
          email = extract_email_from_auth_hash(auth_hash)
    #{"      "}
          # Try to find existing user by email if available
          user = find_by(email: email) if email.present?
    #{"      "}
          # Create a new user if none found
          if user.nil?
            # Convert to HashWithIndifferentAccess for reliable key access
            info = auth_hash[:info].with_indifferent_access if auth_hash[:info]
            claims = auth_hash[:id_token_claims].with_indifferent_access if auth_hash[:id_token_claims]
    #{"        "}
            user = new(
              email: email
              # Add other required fields for your User model here, for example:
              ##{" "}
              # With HashWithIndifferentAccess, access is reliable regardless of key type:
              # first_name: info&.dig(:given_name) || info&.dig(:first_name),
              # last_name: info&.dig(:family_name) || info&.dig(:last_name),
              # name: info&.dig(:name),
              # username: info&.dig(:nickname),
              # avatar_url: info&.dig(:picture) || info&.dig(:image),
              # terms_accepted: true # for required boolean fields
            )
    #{"        "}
            # Mark this user as coming from OAuth to skip password validation
            # This works with the validation conditionals defined above
            user.skip_password_validation = true
    #{"        "}
            # APPROACH 1 & 2: Use standard save with conditional validation
            user.save!
    #{"        "}
            # APPROACH 3: Bypass validations entirely - use with caution, and only if approaches 1 & 2 don't work
            # If your User model has complex validations that are incompatible with OAuth users,
            # you might need to bypass validations. Uncomment this if needed:
            # user.save(validate: false)
          end
    #{"  "}
          # Create or update the OAuth identity
          identity = Clavis::OauthIdentity.find_or_initialize_by(
            provider: auth_hash[:provider],
            uid: auth_hash[:id_token_claims]&.dig(:sub) || auth_hash[:uid]
          )
          identity.user = user
          identity.auth_data = auth_hash[:info]
          identity.token = auth_hash.dig(:credentials, :token)
          identity.refresh_token = auth_hash.dig(:credentials, :refresh_token)
          identity.expires_at = auth_hash.dig(:credentials, :expires_at) ? Time.at(auth_hash.dig(:credentials, :expires_at)) : nil
          identity.save!
    #{"  "}
          # Set the oauth_user flag if available
          user.update(oauth_user: true) if user.respond_to?(:oauth_user=)
    #{"      "}
          # Optional: Update any fields on the User model that you want to keep in sync
          # user.update(
          #   avatar_url: auth_hash.dig(:info, :image),
          #   last_oauth_login_at: Time.current,
          #   last_oauth_provider: auth_hash[:provider]
          # )
    #{"  "}
          user
        end
    #{"    "}
        private
    #{"    "}
        # Helper method to extract email from various locations in the auth hash
        def extract_email_from_auth_hash(auth_hash)
          return nil unless auth_hash
    #{"      "}
          # Try to get email from various possible locations
          if auth_hash[:info]&.with_indifferent_access
            info = auth_hash[:info].with_indifferent_access
            return info[:email] if info[:email].present?
          end
    #{"      "}
          if auth_hash[:id_token_claims]&.with_indifferent_access
            claims = auth_hash[:id_token_claims].with_indifferent_access
            return claims[:email] if claims[:email].present?
          end
    #{"      "}
          if auth_hash[:extra]&.dig(:raw_info)&.with_indifferent_access
            raw_info = auth_hash[:extra][:raw_info].with_indifferent_access
            return raw_info[:email] if raw_info[:email].present?
          end
    #{"      "}
          nil
        end
      end
    end
  RUBY

  say_status :create, "Created ClavisUserMethods concern", :green
end

#show_instructionsObject



199
200
201
202
203
204
205
# File 'lib/generators/clavis/user_method/user_method_generator.rb', line 199

def show_instructions
  # The main instructions will be handled by install_generator.rb
  # This is just a simple confirmation of what was done
  say "\nClavis user methods have been added to your User model."
  say "✅ Created app/models/concerns/clavis_user_methods.rb"
  say "✅ Added 'include ClavisUserMethods' to your User model"
end

#update_user_modelObject



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/generators/clavis/user_method/user_method_generator.rb', line 168

def update_user_model
  user_file = "app/models/user.rb"

  if File.exist?(user_file)
    # Check if the concern is already included
    user_content = File.read(user_file)
    if user_content.include?("include ClavisUserMethods")
      say_status :skip, "ClavisUserMethods already included in User model", :yellow
      return
    end

    # Add the concern include to the User model
    inject_into_file user_file, after: "class User < ApplicationRecord\n" do
      "  include ClavisUserMethods\n"
    end

    say_status :inject, "Added ClavisUserMethods include to User model", :green
  else
    # Create a User model with the concern included
    create_file user_file, <<~RUBY
      class User < ApplicationRecord
        include ClavisUserMethods
      #{"  "}
        # Add your User model code here
      end
    RUBY

    say_status :create, "Created User model with ClavisUserMethods included", :green
  end
end