clavis 0.7.1

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.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.actrc +4 -0
  3. data/.cursor/rules/ruby-gem.mdc +49 -0
  4. data/.gemignore +6 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +88 -0
  7. data/.vscode/settings.json +22 -0
  8. data/CHANGELOG.md +127 -0
  9. data/CODE_OF_CONDUCT.md +3 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +838 -0
  12. data/Rakefile +341 -0
  13. data/UPGRADE.md +57 -0
  14. data/app/assets/stylesheets/clavis.css +133 -0
  15. data/app/controllers/clavis/auth_controller.rb +133 -0
  16. data/config/database.yml +16 -0
  17. data/config/routes.rb +49 -0
  18. data/docs/SECURITY.md +340 -0
  19. data/docs/TESTING.md +78 -0
  20. data/docs/integration.md +272 -0
  21. data/error_handling.md +355 -0
  22. data/file_structure.md +221 -0
  23. data/gemfiles/rails_80.gemfile +17 -0
  24. data/gemfiles/rails_80.gemfile.lock +286 -0
  25. data/implementation_plan.md +523 -0
  26. data/lib/clavis/configuration.rb +196 -0
  27. data/lib/clavis/controllers/concerns/authentication.rb +232 -0
  28. data/lib/clavis/controllers/concerns/session_management.rb +117 -0
  29. data/lib/clavis/engine.rb +191 -0
  30. data/lib/clavis/errors.rb +205 -0
  31. data/lib/clavis/logging.rb +116 -0
  32. data/lib/clavis/models/concerns/oauth_authenticatable.rb +169 -0
  33. data/lib/clavis/oauth_identity.rb +174 -0
  34. data/lib/clavis/providers/apple.rb +135 -0
  35. data/lib/clavis/providers/base.rb +432 -0
  36. data/lib/clavis/providers/custom_provider_example.rb +57 -0
  37. data/lib/clavis/providers/facebook.rb +84 -0
  38. data/lib/clavis/providers/generic.rb +63 -0
  39. data/lib/clavis/providers/github.rb +87 -0
  40. data/lib/clavis/providers/google.rb +98 -0
  41. data/lib/clavis/providers/microsoft.rb +57 -0
  42. data/lib/clavis/security/csrf_protection.rb +79 -0
  43. data/lib/clavis/security/https_enforcer.rb +90 -0
  44. data/lib/clavis/security/input_validator.rb +192 -0
  45. data/lib/clavis/security/parameter_filter.rb +64 -0
  46. data/lib/clavis/security/rate_limiter.rb +109 -0
  47. data/lib/clavis/security/redirect_uri_validator.rb +124 -0
  48. data/lib/clavis/security/session_manager.rb +220 -0
  49. data/lib/clavis/security/token_storage.rb +114 -0
  50. data/lib/clavis/user_info_normalizer.rb +74 -0
  51. data/lib/clavis/utils/nonce_store.rb +14 -0
  52. data/lib/clavis/utils/secure_token.rb +17 -0
  53. data/lib/clavis/utils/state_store.rb +18 -0
  54. data/lib/clavis/version.rb +6 -0
  55. data/lib/clavis/view_helpers.rb +260 -0
  56. data/lib/clavis.rb +132 -0
  57. data/lib/generators/clavis/controller/controller_generator.rb +48 -0
  58. data/lib/generators/clavis/controller/templates/controller.rb.tt +137 -0
  59. data/lib/generators/clavis/controller/templates/views/login.html.erb.tt +145 -0
  60. data/lib/generators/clavis/install_generator.rb +182 -0
  61. data/lib/generators/clavis/templates/add_oauth_to_users.rb +28 -0
  62. data/lib/generators/clavis/templates/clavis.css +133 -0
  63. data/lib/generators/clavis/templates/initializer.rb +47 -0
  64. data/lib/generators/clavis/templates/initializer.rb.tt +76 -0
  65. data/lib/generators/clavis/templates/migration.rb +18 -0
  66. data/lib/generators/clavis/templates/migration.rb.tt +16 -0
  67. data/lib/generators/clavis/user_method/user_method_generator.rb +219 -0
  68. data/lib/tasks/provider_verification.rake +77 -0
  69. data/llms.md +487 -0
  70. data/log/development.log +20 -0
  71. data/log/test.log +0 -0
  72. data/sig/clavis.rbs +4 -0
  73. data/testing_plan.md +710 -0
  74. metadata +258 -0
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clavis
4
+ # Normalizes user information from different OAuth providers into a standard format
5
+ class UserInfoNormalizer
6
+ # Takes raw provider user_info and extracts standard fields
7
+ def self.normalize(provider_name, user_info)
8
+ return {} unless user_info.is_a?(Hash)
9
+
10
+ {
11
+ email: extract_email(provider_name, user_info),
12
+ name: extract_name(provider_name, user_info),
13
+ avatar_url: extract_avatar(provider_name, user_info)
14
+ }
15
+ end
16
+
17
+ def self.extract_email(provider, user_info)
18
+ # Handle both string and symbol keys
19
+ email = user_info[:email] || user_info["email"]
20
+
21
+ # Provider-specific handling
22
+ case provider.to_sym
23
+ when :apple
24
+ email || user_info[:email_verified] || user_info["email_verified"]
25
+ else
26
+ # Default behavior for all other providers (google, github, facebook, microsoft)
27
+ email
28
+ end
29
+ end
30
+
31
+ def self.extract_name(provider, user_info)
32
+ # Handle both string and symbol keys
33
+ name = user_info[:name] || user_info["name"]
34
+ first_name = user_info[:first_name] || user_info["first_name"]
35
+ last_name = user_info[:last_name] || user_info["last_name"]
36
+
37
+ # Provider-specific handling
38
+ case provider.to_sym
39
+ when :google, :github, :facebook, :microsoft
40
+ name
41
+ when :apple
42
+ if name && !name.empty?
43
+ name
44
+ elsif (first_name && !first_name.empty?) || (last_name && !last_name.empty?)
45
+ [first_name, last_name].compact.join(" ")
46
+ end
47
+ else
48
+ name || [first_name, last_name].compact.join(" ")
49
+ end
50
+ end
51
+
52
+ def self.extract_avatar(provider, user_info)
53
+ # Handle various avatar field names across providers
54
+ case provider.to_sym
55
+ when :google, :facebook
56
+ user_info[:picture] || user_info["picture"]
57
+ when :github
58
+ user_info[:avatar_url] || user_info["avatar_url"]
59
+ when :microsoft
60
+ user_info[:avatar] || user_info["avatar"]
61
+ else
62
+ # Try common field names
63
+ user_info[:picture] ||
64
+ user_info["picture"] ||
65
+ user_info[:avatar_url] ||
66
+ user_info["avatar_url"] ||
67
+ user_info[:avatar] ||
68
+ user_info["avatar"] ||
69
+ user_info[:image] ||
70
+ user_info["image"]
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is kept for backward compatibility
4
+ # The functionality has been moved to Clavis::Security::CsrfProtection
5
+
6
+ module Clavis
7
+ module Utils
8
+ module NonceStore
9
+ def self.generate_nonce
10
+ Clavis::Security::CsrfProtection.generate_nonce
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Clavis
6
+ module Utils
7
+ module SecureToken
8
+ def self.generate_state
9
+ SecureRandom.hex(24)
10
+ end
11
+
12
+ def self.generate_nonce
13
+ SecureRandom.hex(16)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is kept for backward compatibility
4
+ # The functionality has been moved to Clavis::Security::CsrfProtection
5
+
6
+ module Clavis
7
+ module Utils
8
+ module StateStore
9
+ def self.generate_state
10
+ Clavis::Security::CsrfProtection.generate_state
11
+ end
12
+
13
+ def self.validate_state!(actual_state, expected_state)
14
+ Clavis::Security::CsrfProtection.validate_state!(actual_state, expected_state)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clavis
4
+ # The current version of Clavis.
5
+ VERSION = "0.7.1"
6
+ end
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clavis
4
+ module ViewHelpers
5
+ # Generates an OAuth button for the specified provider
6
+ #
7
+ # @param provider [Symbol] The provider to generate a button for
8
+ # @param options [Hash] Options for the button
9
+ # @option options [String] :text Custom text for the button
10
+ # @option options [String] :class Custom CSS class for the button
11
+ # @option options [String] :icon Custom icon for the button
12
+ # @option options [String] :icon_class Custom CSS class for the icon
13
+ # @option options [String] :method HTTP method for the button (default: :get)
14
+ # @option options [Hash] :html HTML attributes for the button
15
+ # @return [String] HTML for the button
16
+ def clavis_oauth_button(provider, options = {})
17
+ provider = provider.to_sym
18
+
19
+ # Default options
20
+ options = {
21
+ text: clavis_default_button_text(provider),
22
+ class: clavis_default_button_class(provider),
23
+ icon: clavis_default_button_icon(provider),
24
+ icon_class: clavis_default_icon_class(provider),
25
+ method: :get,
26
+ html: {}
27
+ }.merge(options)
28
+
29
+ # More aggressive Turbo disabling - ensure it works in all environments
30
+ options[:html] ||= {}
31
+ options[:html]["data-turbo"] = "false"
32
+ options[:html]["data-turbo-frame"] = "_top"
33
+ options[:html]["rel"] = "nofollow"
34
+
35
+ # Generate the button with a direct path to the auth endpoint
36
+ clavis_link_to(
37
+ clavis_oauth_button_content(provider, options),
38
+ clavis_auth_authorize_path(provider),
39
+ method: options[:method],
40
+ class: options[:class],
41
+ **options[:html]
42
+ ).html_safe
43
+ end
44
+
45
+ private
46
+
47
+ def clavis_oauth_button_content(_provider, options)
48
+ content = ""
49
+
50
+ # Add icon if available
51
+ if options[:icon].present?
52
+ icon_html = clavis_provider_svg(options[:icon])
53
+ content += clavis_content_tag(:span, icon_html.html_safe, class: options[:icon_class])
54
+ end
55
+
56
+ # Add text
57
+ content += clavis_content_tag(:span, options[:text], class: "clavis-oauth-button__text")
58
+
59
+ content
60
+ end
61
+
62
+ def clavis_auth_path(provider)
63
+ if defined?(clavis) && clavis.respond_to?("auth_#{provider}_path")
64
+ # Use the engine routing proxy if available
65
+ clavis.send("auth_#{provider}_path")
66
+ elsif defined?(clavis) && clavis.respond_to?(:auth_path)
67
+ # Fallback to generic auth path with provider param
68
+ clavis.auth_path(provider: provider)
69
+ else
70
+ # Last resort: construct the path manually
71
+ # This path is relative to the engine mount point
72
+ "/#{provider}"
73
+ end
74
+ end
75
+
76
+ def clavis_default_button_text(provider)
77
+ case provider
78
+ when :google
79
+ "Sign in with Google"
80
+ when :github
81
+ "Sign in with GitHub"
82
+ when :facebook
83
+ "Sign in with Facebook"
84
+ when :apple
85
+ "Sign in with Apple"
86
+ when :microsoft
87
+ "Sign in with Microsoft"
88
+ else
89
+ "Sign in with #{provider.to_s.titleize}"
90
+ end
91
+ end
92
+
93
+ def clavis_default_button_class(provider)
94
+ "clavis-oauth-button clavis-oauth-button--#{provider}"
95
+ end
96
+
97
+ def clavis_default_button_icon(provider)
98
+ case provider
99
+ when :google
100
+ "google"
101
+ when :github
102
+ "github"
103
+ when :facebook
104
+ "facebook"
105
+ when :apple
106
+ "apple"
107
+ when :microsoft
108
+ "microsoft"
109
+ else
110
+ "oauth"
111
+ end
112
+ end
113
+
114
+ def clavis_default_icon_class(provider)
115
+ "clavis-oauth-button__icon clavis-oauth-button__icon--#{provider}"
116
+ end
117
+
118
+ def clavis_provider_svg(provider)
119
+ case provider.to_sym
120
+ when :google
121
+ <<~SVG.html_safe
122
+ <svg class="clavis-icon" width="18" height="18" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
123
+ <path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/>
124
+ <path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/>
125
+ <path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/>
126
+ <path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/>
127
+ <path fill="none" d="M0 0h48v48H0z"/>
128
+ </svg>
129
+ SVG
130
+ when :github
131
+ <<~SVG.html_safe
132
+ <svg class="clavis-icon" width="18" height="18" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
133
+ <path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.82-1.22-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
134
+ </svg>
135
+ SVG
136
+ when :apple
137
+ <<~SVG.html_safe
138
+ <svg class="clavis-icon" width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512">
139
+ <path fill="currentColor" d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"/>
140
+ </svg>
141
+ SVG
142
+ when :facebook
143
+ <<~SVG.html_safe
144
+ <svg class="clavis-icon" width="18" height="18" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
145
+ <path fill="currentColor" d="M279.14 288l14.22-92.66h-88.91v-60.13c0-25.35 12.42-50.06 52.24-50.06h40.42V6.26S260.43 0 225.36 0c-73.22 0-121.08 44.38-121.08 124.72v70.62H22.89V288h81.39v224h100.17V288z"/>
146
+ </svg>
147
+ SVG
148
+ when :microsoft
149
+ <<~SVG.html_safe
150
+ <svg class="clavis-icon" width="18" height="18" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
151
+ <path fill="#f35325" d="M1 1h10v10H1z"/>
152
+ <path fill="#81bc06" d="M12 1h10v10H12z"/>
153
+ <path fill="#05a6f0" d="M1 12h10v10H1z"/>
154
+ <path fill="#ffba08" d="M12 12h10v10H12z"/>
155
+ </svg>
156
+ SVG
157
+ else
158
+ clavis_content_tag(:div, "", class: "clavis-icon clavis-icon-#{provider}")
159
+ end
160
+ end
161
+
162
+ def clavis_auth_authorize_path(provider)
163
+ # Explicitly add /auth prefix for direct calls
164
+ "/auth/#{provider}"
165
+ end
166
+
167
+ # Rails helper methods for non-Rails environments
168
+ def clavis_content_tag(tag, content_or_options_with_block = nil, options = nil, &)
169
+ if block_given?
170
+ options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
171
+ content = capture(&)
172
+ else
173
+ content = content_or_options_with_block
174
+ end
175
+
176
+ options ||= {}
177
+ tag_options = clavis_tag_options(options)
178
+
179
+ "<#{tag}#{tag_options}>#{content}</#{tag}>"
180
+ end
181
+
182
+ def clavis_link_to(name = nil, options = nil, html_options = nil, &)
183
+ if block_given?
184
+ html_options = options
185
+ options = name
186
+ name = capture(&)
187
+ end
188
+ options ||= {}
189
+ html_options ||= {}
190
+
191
+ url = clavis_url_for(options)
192
+ html_options = clavis_convert_options_to_data_attributes(options, html_options)
193
+ tag_options = clavis_tag_options(html_options)
194
+
195
+ href = "href=\"#{url}\"" unless url.nil?
196
+ "<a #{href}#{tag_options}>#{name}</a>"
197
+ end
198
+
199
+ def clavis_url_for(options)
200
+ case options
201
+ when String
202
+ options
203
+ when Hash
204
+ options[:controller] ||= controller_name
205
+ options[:action] ||= action_name
206
+
207
+ path = "/#{options[:controller]}/#{options[:action]}"
208
+ path += "/#{options[:id]}" if options[:id]
209
+ path
210
+ else
211
+ options.to_s
212
+ end
213
+ end
214
+
215
+ def clavis_tag_options(options)
216
+ return "" if options.empty?
217
+
218
+ attrs = []
219
+ options.each_pair do |key, value|
220
+ if key.to_s == "data" && value.is_a?(Hash)
221
+ value.each_pair do |k, v|
222
+ attrs << clavis_data_tag_option("data-#{k}", v)
223
+ end
224
+ elsif key.to_s == "class" && value.is_a?(Array)
225
+ attrs << clavis_tag_option(key, value.join(" "))
226
+ else
227
+ attrs << clavis_tag_option(key, value)
228
+ end
229
+ end
230
+
231
+ " #{attrs.join(" ")}" unless attrs.empty?
232
+ end
233
+
234
+ def clavis_tag_option(key, value)
235
+ "#{key}=\"#{value}\""
236
+ end
237
+
238
+ def clavis_data_tag_option(key, value)
239
+ "#{key}=\"#{value}\""
240
+ end
241
+
242
+ def clavis_convert_options_to_data_attributes(_options, html_options)
243
+ html_options["data-method"] = html_options.delete(:method) if html_options.key?(:method)
244
+
245
+ html_options
246
+ end
247
+
248
+ def clavis_capture
249
+ yield
250
+ end
251
+
252
+ def controller_name
253
+ "auth"
254
+ end
255
+
256
+ def action_name
257
+ "index"
258
+ end
259
+ end
260
+ end
data/lib/clavis.rb ADDED
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "clavis/version"
4
+ require_relative "clavis/configuration"
5
+ require_relative "clavis/errors"
6
+ require_relative "clavis/logging"
7
+ require_relative "clavis/utils/state_store"
8
+ require_relative "clavis/utils/nonce_store"
9
+ require_relative "clavis/security/token_storage"
10
+ require_relative "clavis/security/parameter_filter"
11
+ require_relative "clavis/security/csrf_protection"
12
+ require_relative "clavis/security/redirect_uri_validator"
13
+ require_relative "clavis/security/https_enforcer"
14
+ require_relative "clavis/security/input_validator"
15
+ require_relative "clavis/security/session_manager"
16
+ require_relative "clavis/security/rate_limiter"
17
+ require "clavis/user_info_normalizer"
18
+
19
+ # Only load provider classes if they're not already defined (for testing)
20
+ unless defined?(Clavis::Providers::Base)
21
+ require_relative "clavis/providers/base"
22
+ require_relative "clavis/providers/google"
23
+ require_relative "clavis/providers/github"
24
+ require_relative "clavis/providers/facebook"
25
+ require_relative "clavis/providers/apple"
26
+ require_relative "clavis/providers/microsoft"
27
+ require_relative "clavis/providers/generic"
28
+ end
29
+
30
+ require_relative "clavis/oauth_identity"
31
+ require_relative "clavis/models/concerns/oauth_authenticatable"
32
+ require_relative "clavis/controllers/concerns/authentication"
33
+ require_relative "clavis/controllers/concerns/session_management"
34
+ require_relative "clavis/view_helpers"
35
+
36
+ # Required for delegate method
37
+ require "active_support/core_ext/module/delegation"
38
+
39
+ # Create an alias for backward compatibility
40
+ module Clavis
41
+ module Models
42
+ # Alias for Clavis::Models::Concerns::OauthAuthenticatable
43
+ # This makes it easier to include in user models as documented
44
+ OauthAuthenticatable = Concerns::OauthAuthenticatable
45
+ end
46
+ end
47
+
48
+ # Only load the engine if Rails is defined
49
+ begin
50
+ require_relative "clavis/engine" if defined?(Rails)
51
+ rescue LoadError => e
52
+ # Log a warning if we're unable to load the engine
53
+ warn "Warning: Unable to load Clavis::Engine - #{e.message}" if defined?(Rails)
54
+ end
55
+
56
+ module Clavis
57
+ class << self
58
+ attr_writer :configuration
59
+
60
+ def configure
61
+ yield(configuration) if block_given?
62
+ configuration.post_initialize
63
+ end
64
+
65
+ def configuration
66
+ @configuration ||= Configuration.new
67
+ end
68
+
69
+ def reset_configuration!
70
+ @configuration = Configuration.new
71
+ end
72
+
73
+ def provider(name, options = {})
74
+ name = name.to_sym
75
+
76
+ provider_class = provider_registry[name] ||
77
+ case name
78
+ when :google
79
+ Providers::Google
80
+ when :github
81
+ Providers::Github
82
+ when :facebook
83
+ Providers::Facebook
84
+ when :apple
85
+ Providers::Apple
86
+ when :microsoft
87
+ Providers::Microsoft
88
+ when :generic
89
+ Providers::Generic
90
+ else
91
+ raise UnsupportedProvider, name
92
+ end
93
+
94
+ # Merge options with configuration
95
+ config = configuration.providers[name] || {}
96
+ config = config.merge(options)
97
+
98
+ provider_class.new(config)
99
+ end
100
+
101
+ def register_provider(name, provider_class)
102
+ provider_registry[name.to_sym] = provider_class
103
+ end
104
+
105
+ def provider_registry
106
+ @provider_registry ||= {}
107
+ end
108
+
109
+ # Define logger methods manually instead of using delegate
110
+ def logger
111
+ Logging.logger
112
+ end
113
+
114
+ def logger=(value)
115
+ Logging.logger = value
116
+ end
117
+
118
+ def self.setup
119
+ yield(configuration) if block_given?
120
+ configuration.post_initialize
121
+ self
122
+ end
123
+ end
124
+ end
125
+
126
+ # Register built-in providers
127
+ Clavis.register_provider(:google, Clavis::Providers::Google)
128
+ Clavis.register_provider(:github, Clavis::Providers::Github)
129
+ Clavis.register_provider(:facebook, Clavis::Providers::Facebook)
130
+ Clavis.register_provider(:apple, Clavis::Providers::Apple)
131
+ Clavis.register_provider(:microsoft, Clavis::Providers::Microsoft)
132
+ Clavis.register_provider(:generic, Clavis::Providers::Generic)
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Clavis
6
+ module Generators
7
+ class ControllerGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ argument :controller_name, type: :string, default: "Auth"
11
+
12
+ class_option :skip_routes, type: :boolean, default: false, desc: "Skip route generation"
13
+
14
+ def create_controller
15
+ template "controller.rb.tt", "app/controllers/#{file_name}_controller.rb"
16
+ end
17
+
18
+ def create_views
19
+ template "views/login.html.erb.tt", "app/views/#{file_name}/login.html.erb"
20
+ end
21
+
22
+ def add_routes
23
+ return if options[:skip_routes]
24
+
25
+ route_config = <<~ROUTES
26
+ # OAuth routes
27
+ get '/auth/:provider', to: '#{file_name}#authorize', as: :auth
28
+ get '/auth/:provider/callback', to: '#{file_name}#callback', as: :auth_callback
29
+ get '/auth/failure', to: '#{file_name}#failure', as: :auth_failure
30
+ get '/login', to: '#{file_name}#login', as: :login
31
+ delete '/logout', to: '#{file_name}#logout', as: :logout
32
+ ROUTES
33
+
34
+ route route_config
35
+ end
36
+
37
+ private
38
+
39
+ def file_name
40
+ controller_name.underscore
41
+ end
42
+
43
+ def class_name
44
+ controller_name.camelize
45
+ end
46
+ end
47
+ end
48
+ end