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.
- checksums.yaml +7 -0
- data/.actrc +4 -0
- data/.cursor/rules/ruby-gem.mdc +49 -0
- data/.gemignore +6 -0
- data/.rspec +3 -0
- data/.rubocop.yml +88 -0
- data/.vscode/settings.json +22 -0
- data/CHANGELOG.md +127 -0
- data/CODE_OF_CONDUCT.md +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +838 -0
- data/Rakefile +341 -0
- data/UPGRADE.md +57 -0
- data/app/assets/stylesheets/clavis.css +133 -0
- data/app/controllers/clavis/auth_controller.rb +133 -0
- data/config/database.yml +16 -0
- data/config/routes.rb +49 -0
- data/docs/SECURITY.md +340 -0
- data/docs/TESTING.md +78 -0
- data/docs/integration.md +272 -0
- data/error_handling.md +355 -0
- data/file_structure.md +221 -0
- data/gemfiles/rails_80.gemfile +17 -0
- data/gemfiles/rails_80.gemfile.lock +286 -0
- data/implementation_plan.md +523 -0
- data/lib/clavis/configuration.rb +196 -0
- data/lib/clavis/controllers/concerns/authentication.rb +232 -0
- data/lib/clavis/controllers/concerns/session_management.rb +117 -0
- data/lib/clavis/engine.rb +191 -0
- data/lib/clavis/errors.rb +205 -0
- data/lib/clavis/logging.rb +116 -0
- data/lib/clavis/models/concerns/oauth_authenticatable.rb +169 -0
- data/lib/clavis/oauth_identity.rb +174 -0
- data/lib/clavis/providers/apple.rb +135 -0
- data/lib/clavis/providers/base.rb +432 -0
- data/lib/clavis/providers/custom_provider_example.rb +57 -0
- data/lib/clavis/providers/facebook.rb +84 -0
- data/lib/clavis/providers/generic.rb +63 -0
- data/lib/clavis/providers/github.rb +87 -0
- data/lib/clavis/providers/google.rb +98 -0
- data/lib/clavis/providers/microsoft.rb +57 -0
- data/lib/clavis/security/csrf_protection.rb +79 -0
- data/lib/clavis/security/https_enforcer.rb +90 -0
- data/lib/clavis/security/input_validator.rb +192 -0
- data/lib/clavis/security/parameter_filter.rb +64 -0
- data/lib/clavis/security/rate_limiter.rb +109 -0
- data/lib/clavis/security/redirect_uri_validator.rb +124 -0
- data/lib/clavis/security/session_manager.rb +220 -0
- data/lib/clavis/security/token_storage.rb +114 -0
- data/lib/clavis/user_info_normalizer.rb +74 -0
- data/lib/clavis/utils/nonce_store.rb +14 -0
- data/lib/clavis/utils/secure_token.rb +17 -0
- data/lib/clavis/utils/state_store.rb +18 -0
- data/lib/clavis/version.rb +6 -0
- data/lib/clavis/view_helpers.rb +260 -0
- data/lib/clavis.rb +132 -0
- data/lib/generators/clavis/controller/controller_generator.rb +48 -0
- data/lib/generators/clavis/controller/templates/controller.rb.tt +137 -0
- data/lib/generators/clavis/controller/templates/views/login.html.erb.tt +145 -0
- data/lib/generators/clavis/install_generator.rb +182 -0
- data/lib/generators/clavis/templates/add_oauth_to_users.rb +28 -0
- data/lib/generators/clavis/templates/clavis.css +133 -0
- data/lib/generators/clavis/templates/initializer.rb +47 -0
- data/lib/generators/clavis/templates/initializer.rb.tt +76 -0
- data/lib/generators/clavis/templates/migration.rb +18 -0
- data/lib/generators/clavis/templates/migration.rb.tt +16 -0
- data/lib/generators/clavis/user_method/user_method_generator.rb +219 -0
- data/lib/tasks/provider_verification.rake +77 -0
- data/llms.md +487 -0
- data/log/development.log +20 -0
- data/log/test.log +0 -0
- data/sig/clavis.rbs +4 -0
- data/testing_plan.md +710 -0
- metadata +258 -0
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class <%= class_name %>Controller < ApplicationController
|
4
|
+
include Clavis::Controllers::Concerns::Authentication
|
5
|
+
include Clavis::Controllers::Concerns::SessionManagement
|
6
|
+
|
7
|
+
# Skip CSRF protection for callback action since OAuth providers will redirect to it
|
8
|
+
# The OAuth flow has its own CSRF protection via the state parameter
|
9
|
+
skip_before_action :verify_authenticity_token, only: [:callback]
|
10
|
+
|
11
|
+
# Skip authentication for OAuth actions
|
12
|
+
skip_before_action :authenticate_user!, only: [:login, :authorize, :callback, :failure], if: -> { respond_to?(:authenticate_user!) }
|
13
|
+
|
14
|
+
# Login page with OAuth provider buttons
|
15
|
+
def login
|
16
|
+
# If user is already logged in, redirect to root path
|
17
|
+
redirect_to after_login_path if authenticated?
|
18
|
+
end
|
19
|
+
|
20
|
+
# Start OAuth flow
|
21
|
+
def authorize
|
22
|
+
# Store the current URL to return to after authentication if needed
|
23
|
+
store_location if params[:return_to].blank?
|
24
|
+
|
25
|
+
# Store the return_to path in the session if explicitly provided
|
26
|
+
if params[:return_to].present?
|
27
|
+
Clavis::Security::SessionManager.store_redirect_uri(session, params[:return_to])
|
28
|
+
end
|
29
|
+
|
30
|
+
# Start the OAuth flow
|
31
|
+
oauth_authorize
|
32
|
+
rescue => e
|
33
|
+
handle_error(e)
|
34
|
+
end
|
35
|
+
|
36
|
+
# OAuth callback
|
37
|
+
def callback
|
38
|
+
oauth_callback do |user, auth_hash|
|
39
|
+
# Find or create user from OAuth data
|
40
|
+
@user = find_or_create_user(auth_hash)
|
41
|
+
|
42
|
+
# Sign in the user using the secure cookie approach
|
43
|
+
sign_in_user(@user)
|
44
|
+
|
45
|
+
# Redirect to the stored redirect URI or default path
|
46
|
+
redirect_uri = Clavis::Security::SessionManager.validate_and_retrieve_redirect_uri(
|
47
|
+
session,
|
48
|
+
default: after_login_path
|
49
|
+
)
|
50
|
+
|
51
|
+
redirect_to redirect_uri
|
52
|
+
end
|
53
|
+
rescue => e
|
54
|
+
handle_error(e)
|
55
|
+
end
|
56
|
+
|
57
|
+
# OAuth failure
|
58
|
+
def failure
|
59
|
+
message = params[:message] || "Authentication failed"
|
60
|
+
flash[:alert] = Clavis::Security::InputValidator.sanitize(message)
|
61
|
+
redirect_to login_path
|
62
|
+
end
|
63
|
+
|
64
|
+
# Logout
|
65
|
+
def logout
|
66
|
+
sign_out_user
|
67
|
+
redirect_to after_logout_path
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# Find or create a user from OAuth data
|
73
|
+
def find_or_create_user(auth_hash)
|
74
|
+
# Find existing identity
|
75
|
+
identity = OauthIdentity.find_by(
|
76
|
+
provider: auth_hash[:provider],
|
77
|
+
uid: auth_hash[:uid]
|
78
|
+
)
|
79
|
+
|
80
|
+
if identity&.user
|
81
|
+
# Update the identity with new token information
|
82
|
+
identity.update(
|
83
|
+
token: auth_hash[:credentials][:token],
|
84
|
+
refresh_token: auth_hash[:credentials][:refresh_token],
|
85
|
+
expires_at: auth_hash[:credentials][:expires_at]
|
86
|
+
)
|
87
|
+
|
88
|
+
return identity.user
|
89
|
+
else
|
90
|
+
# Create a new user and identity
|
91
|
+
user = User.find_or_create_from_clavis(auth_hash)
|
92
|
+
|
93
|
+
# Create the identity
|
94
|
+
OauthIdentity.create(
|
95
|
+
user: user,
|
96
|
+
provider: auth_hash[:provider],
|
97
|
+
uid: auth_hash[:uid],
|
98
|
+
token: auth_hash[:credentials][:token],
|
99
|
+
refresh_token: auth_hash[:credentials][:refresh_token],
|
100
|
+
expires_at: auth_hash[:credentials][:expires_at]
|
101
|
+
)
|
102
|
+
|
103
|
+
return user
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Handle errors
|
108
|
+
def handle_error(error)
|
109
|
+
case error
|
110
|
+
when Clavis::AuthorizationDenied
|
111
|
+
flash[:alert] = "Authorization denied: #{error.message}"
|
112
|
+
redirect_to login_path
|
113
|
+
when Clavis::InvalidState
|
114
|
+
flash[:alert] = "Invalid state parameter. Please try again."
|
115
|
+
redirect_to login_path
|
116
|
+
when Clavis::InvalidGrant
|
117
|
+
flash[:alert] = "Invalid authorization code. Please try again."
|
118
|
+
redirect_to login_path
|
119
|
+
when Clavis::InvalidNonce
|
120
|
+
flash[:alert] = "Invalid nonce parameter. Please try again."
|
121
|
+
redirect_to login_path
|
122
|
+
when Clavis::InvalidRedirectUri
|
123
|
+
flash[:alert] = "Invalid redirect URI. Please try again."
|
124
|
+
redirect_to login_path
|
125
|
+
when Clavis::ProviderError
|
126
|
+
flash[:alert] = "Provider error: #{error.message}"
|
127
|
+
redirect_to login_path
|
128
|
+
else
|
129
|
+
# Log the error
|
130
|
+
Rails.logger.error("OAuth Error: #{error.class.name} - #{error.message}")
|
131
|
+
Rails.logger.error(error.backtrace.join("\n"))
|
132
|
+
|
133
|
+
flash[:alert] = "An error occurred during authentication. Please try again."
|
134
|
+
redirect_to login_path
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
<div class="oauth-login-container">
|
2
|
+
<h1>Sign in with</h1>
|
3
|
+
|
4
|
+
<% if flash[:alert] %>
|
5
|
+
<div class="alert alert-danger">
|
6
|
+
<%= flash[:alert] %>
|
7
|
+
</div>
|
8
|
+
<% end %>
|
9
|
+
|
10
|
+
<div class="oauth-providers">
|
11
|
+
<% if Clavis.configuration.providers[:google] %>
|
12
|
+
<a href="<%= auth_path(:google) %>" class="oauth-button google-button">
|
13
|
+
<span class="provider-icon">G</span>
|
14
|
+
<span class="provider-name">Google</span>
|
15
|
+
</a>
|
16
|
+
<% end %>
|
17
|
+
|
18
|
+
<% if Clavis.configuration.providers[:github] %>
|
19
|
+
<a href="<%= auth_path(:github) %>" class="oauth-button github-button">
|
20
|
+
<span class="provider-icon">GH</span>
|
21
|
+
<span class="provider-name">GitHub</span>
|
22
|
+
</a>
|
23
|
+
<% end %>
|
24
|
+
|
25
|
+
<% if Clavis.configuration.providers[:facebook] %>
|
26
|
+
<a href="<%= auth_path(:facebook) %>" class="oauth-button facebook-button">
|
27
|
+
<span class="provider-icon">F</span>
|
28
|
+
<span class="provider-name">Facebook</span>
|
29
|
+
</a>
|
30
|
+
<% end %>
|
31
|
+
|
32
|
+
<% if Clavis.configuration.providers[:apple] %>
|
33
|
+
<a href="<%= auth_path(:apple) %>" class="oauth-button apple-button">
|
34
|
+
<span class="provider-icon">A</span>
|
35
|
+
<span class="provider-name">Apple</span>
|
36
|
+
</a>
|
37
|
+
<% end %>
|
38
|
+
|
39
|
+
<% if Clavis.configuration.providers[:microsoft] %>
|
40
|
+
<a href="<%= auth_path(:microsoft) %>" class="oauth-button microsoft-button">
|
41
|
+
<span class="provider-icon">M</span>
|
42
|
+
<span class="provider-name">Microsoft</span>
|
43
|
+
</a>
|
44
|
+
<% end %>
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
|
48
|
+
<style>
|
49
|
+
.oauth-login-container {
|
50
|
+
max-width: 400px;
|
51
|
+
margin: 0 auto;
|
52
|
+
padding: 2rem;
|
53
|
+
text-align: center;
|
54
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
55
|
+
}
|
56
|
+
|
57
|
+
.oauth-login-container h1 {
|
58
|
+
margin-bottom: 2rem;
|
59
|
+
font-weight: 500;
|
60
|
+
}
|
61
|
+
|
62
|
+
.alert {
|
63
|
+
padding: 1rem;
|
64
|
+
margin-bottom: 1rem;
|
65
|
+
border-radius: 4px;
|
66
|
+
}
|
67
|
+
|
68
|
+
.alert-danger {
|
69
|
+
background-color: #f8d7da;
|
70
|
+
color: #721c24;
|
71
|
+
border: 1px solid #f5c6cb;
|
72
|
+
}
|
73
|
+
|
74
|
+
.oauth-providers {
|
75
|
+
display: flex;
|
76
|
+
flex-direction: column;
|
77
|
+
gap: 1rem;
|
78
|
+
}
|
79
|
+
|
80
|
+
.oauth-button {
|
81
|
+
display: flex;
|
82
|
+
align-items: center;
|
83
|
+
padding: 0.75rem 1rem;
|
84
|
+
border-radius: 4px;
|
85
|
+
text-decoration: none;
|
86
|
+
font-weight: 500;
|
87
|
+
transition: background-color 0.2s;
|
88
|
+
}
|
89
|
+
|
90
|
+
.provider-icon {
|
91
|
+
display: flex;
|
92
|
+
align-items: center;
|
93
|
+
justify-content: center;
|
94
|
+
width: 24px;
|
95
|
+
height: 24px;
|
96
|
+
margin-right: 1rem;
|
97
|
+
border-radius: 50%;
|
98
|
+
background-color: rgba(255, 255, 255, 0.2);
|
99
|
+
}
|
100
|
+
|
101
|
+
.google-button {
|
102
|
+
background-color: #4285F4;
|
103
|
+
color: white;
|
104
|
+
}
|
105
|
+
|
106
|
+
.google-button:hover {
|
107
|
+
background-color: #3367D6;
|
108
|
+
}
|
109
|
+
|
110
|
+
.github-button {
|
111
|
+
background-color: #24292e;
|
112
|
+
color: white;
|
113
|
+
}
|
114
|
+
|
115
|
+
.github-button:hover {
|
116
|
+
background-color: #1a1e22;
|
117
|
+
}
|
118
|
+
|
119
|
+
.facebook-button {
|
120
|
+
background-color: #1877F2;
|
121
|
+
color: white;
|
122
|
+
}
|
123
|
+
|
124
|
+
.facebook-button:hover {
|
125
|
+
background-color: #166fe5;
|
126
|
+
}
|
127
|
+
|
128
|
+
.apple-button {
|
129
|
+
background-color: #000000;
|
130
|
+
color: white;
|
131
|
+
}
|
132
|
+
|
133
|
+
.apple-button:hover {
|
134
|
+
background-color: #1a1a1a;
|
135
|
+
}
|
136
|
+
|
137
|
+
.microsoft-button {
|
138
|
+
background-color: #00a4ef;
|
139
|
+
color: white;
|
140
|
+
}
|
141
|
+
|
142
|
+
.microsoft-button:hover {
|
143
|
+
background-color: #0078d4;
|
144
|
+
}
|
145
|
+
</style>
|
@@ -0,0 +1,182 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/base"
|
4
|
+
require "rails/generators/active_record"
|
5
|
+
require "rails/generators/actions"
|
6
|
+
|
7
|
+
module Clavis
|
8
|
+
module Generators
|
9
|
+
class InstallGenerator < Rails::Generators::Base
|
10
|
+
include ActiveRecord::Generators::Migration
|
11
|
+
include Rails::Generators::Actions
|
12
|
+
|
13
|
+
source_root File.expand_path("templates", __dir__)
|
14
|
+
|
15
|
+
class_option :providers, type: :array, default: ["google"],
|
16
|
+
desc: "List of providers to configure (google, github, apple, facebook, microsoft)"
|
17
|
+
|
18
|
+
# Implement the required next_migration_number method
|
19
|
+
# Must be defined as a class method
|
20
|
+
def self.next_migration_number(dirname)
|
21
|
+
next_migration_number = current_migration_number(dirname) + 1
|
22
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_initializer
|
26
|
+
template "initializer.rb", "config/initializers/clavis.rb"
|
27
|
+
say_status :create, "config/initializers/clavis.rb", :green
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_stylesheets
|
31
|
+
# Create the vendor directory if it doesn't exist
|
32
|
+
vendor_css_dir = Rails.root.join("vendor", "assets", "stylesheets")
|
33
|
+
FileUtils.mkdir_p(vendor_css_dir) unless File.directory?(vendor_css_dir)
|
34
|
+
|
35
|
+
# Copy the CSS template to the vendor directory
|
36
|
+
template "clavis.css", "vendor/assets/stylesheets/clavis.css"
|
37
|
+
say_status :create, "vendor/assets/stylesheets/clavis.css", :green
|
38
|
+
|
39
|
+
# Create custom styles file in app/assets
|
40
|
+
create_file "app/assets/stylesheets/clavis_custom.css", "/* Add your custom Clavis styles here */"
|
41
|
+
say_status :create, "app/assets/stylesheets/clavis_custom.css", :green
|
42
|
+
|
43
|
+
# For Rails 7+ with Propshaft
|
44
|
+
if File.exist?(Rails.root.join("app", "assets", "stylesheets", "application.css"))
|
45
|
+
app_css_content = File.read(Rails.root.join("app", "assets", "stylesheets", "application.css"))
|
46
|
+
if app_css_content.include?("Propshaft")
|
47
|
+
# Create a separate file for Propshaft
|
48
|
+
propshaft_css_path = Rails.root.join("app", "assets", "stylesheets", "clavis_styles.css")
|
49
|
+
create_file propshaft_css_path, File.read(File.expand_path("clavis.css", source_paths.first))
|
50
|
+
say_status :create, "app/assets/stylesheets/clavis_styles.css for Propshaft", :green
|
51
|
+
@provide_css_instructions = true
|
52
|
+
return
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Different strategies for different asset pipeline setups
|
57
|
+
if File.exist?(Rails.root.join("app", "assets", "stylesheets", "application.scss"))
|
58
|
+
append_to_file "app/assets/stylesheets/application.scss", "\n@import 'clavis';\n"
|
59
|
+
say_status :insert, "clavis import in application.scss", :green
|
60
|
+
elsif File.exist?(Rails.root.join("app", "assets", "stylesheets", "application.css"))
|
61
|
+
inject_into_file "app/assets/stylesheets/application.css", " *= require clavis\n", before: "*/",
|
62
|
+
verbose: false
|
63
|
+
say_status :insert, "clavis require in application.css", :green
|
64
|
+
elsif File.exist?(Rails.root.join("app", "assets", "stylesheets", "application.css.scss"))
|
65
|
+
append_to_file "app/assets/stylesheets/application.css.scss", "\n@import 'clavis';\n"
|
66
|
+
say_status :insert, "clavis import in application.css.scss", :green
|
67
|
+
else
|
68
|
+
say_status :warn, "Could not find main application CSS file", :yellow
|
69
|
+
create_file "app/assets/stylesheets/clavis_styles.css",
|
70
|
+
File.read(File.expand_path("clavis.css", source_paths.first))
|
71
|
+
say_status :create, "app/assets/stylesheets/clavis_styles.css as fallback", :green
|
72
|
+
@provide_css_instructions = true
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def create_migration
|
77
|
+
# First, create the OAuth identities table migration if it doesn't exist
|
78
|
+
create_identities_migration
|
79
|
+
|
80
|
+
# Then create the User table migration if the users table exists
|
81
|
+
create_user_migration
|
82
|
+
rescue ActiveRecord::NoDatabaseError
|
83
|
+
say_status :error, "Skipping migration because database doesn't exist. Run 'rails db:create' first.", :red
|
84
|
+
end
|
85
|
+
|
86
|
+
def mount_engine
|
87
|
+
# Check if the route already exists in the routes file
|
88
|
+
routes_content = File.read(Rails.root.join("config/routes.rb"))
|
89
|
+
|
90
|
+
# Only add the route if it doesn't already exist
|
91
|
+
if routes_content.include?("mount Clavis::Engine")
|
92
|
+
say_status :skip, "Clavis::Engine is already mounted, skipping route addition.", :yellow
|
93
|
+
else
|
94
|
+
route "mount Clavis::Engine => '/auth'"
|
95
|
+
say_status :route, "Mounted Clavis::Engine at /auth", :green
|
96
|
+
say_status :info, "Added auth_path and auth_callback_path route helpers", :green
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def create_user_method
|
101
|
+
# Generate the user method concern
|
102
|
+
generate "clavis:user_method"
|
103
|
+
end
|
104
|
+
|
105
|
+
def show_post_install_message
|
106
|
+
say "\nClavis has been installed successfully!"
|
107
|
+
|
108
|
+
# Next steps
|
109
|
+
say "\nNext steps:"
|
110
|
+
|
111
|
+
steps = []
|
112
|
+
|
113
|
+
if @provide_css_instructions
|
114
|
+
steps << "Include the Clavis styles in your layout:\n <%= stylesheet_link_tag 'clavis_styles' %>"
|
115
|
+
end
|
116
|
+
|
117
|
+
steps << "Configure your providers in config/initializers/clavis.rb"
|
118
|
+
steps << "Run migrations: rails db:migrate"
|
119
|
+
steps << "⚠️ Customize the user creation code in app/models/concerns/clavis_user_methods.rb"
|
120
|
+
steps << "Add OAuth buttons to your views:\n <%= clavis_oauth_button :google %>"
|
121
|
+
|
122
|
+
# Output numbered steps
|
123
|
+
steps.each_with_index do |step, index|
|
124
|
+
say "#{index + 1}. #{step}"
|
125
|
+
end
|
126
|
+
|
127
|
+
say "\nClavis has configured your User model with OAuth support via the ClavisUserMethods concern."
|
128
|
+
say "IMPORTANT: The default implementation only sets the email field when creating users."
|
129
|
+
say "You MUST customize this to include all required fields for your User model."
|
130
|
+
|
131
|
+
say "\nFor more information, see the documentation at https://github.com/clayton/clavis"
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def create_identities_migration
|
137
|
+
return if migration_exists?("db/migrate", "create_clavis_oauth_identities")
|
138
|
+
|
139
|
+
migration_number = self.class.next_migration_number("db/migrate")
|
140
|
+
@migration_class_name = "CreateClavisOauthIdentities"
|
141
|
+
template(
|
142
|
+
"migration.rb",
|
143
|
+
"db/migrate/#{migration_number}_create_clavis_oauth_identities.rb"
|
144
|
+
)
|
145
|
+
say_status :migration, "Created db/migrate/#{migration_number}_create_clavis_oauth_identities.rb", :green
|
146
|
+
end
|
147
|
+
|
148
|
+
def create_user_migration
|
149
|
+
return if migration_exists?("db/migrate", "add_oauth_to_users")
|
150
|
+
|
151
|
+
# Check if the users table exists
|
152
|
+
return unless table_exists?("users")
|
153
|
+
|
154
|
+
migration_number = self.class.next_migration_number("db/migrate")
|
155
|
+
@migration_class_name = "AddOauthToUsers"
|
156
|
+
|
157
|
+
template(
|
158
|
+
"add_oauth_to_users.rb",
|
159
|
+
"db/migrate/#{migration_number}_add_oauth_to_users.rb"
|
160
|
+
)
|
161
|
+
say_status :migration, "Created db/migrate/#{migration_number}_add_oauth_to_users.rb", :green
|
162
|
+
end
|
163
|
+
|
164
|
+
# Check if a migration with a given name already exists
|
165
|
+
def migration_exists?(dirname, migration_name)
|
166
|
+
Dir.glob("#{dirname}/[0-9]*_*.rb").grep(/\d+_#{migration_name}.rb$/).any?
|
167
|
+
end
|
168
|
+
|
169
|
+
# Check if a table exists in the database
|
170
|
+
def table_exists?(table_name)
|
171
|
+
ActiveRecord::Base.connection.table_exists?(table_name)
|
172
|
+
rescue ActiveRecord::NoDatabaseError
|
173
|
+
say_status :error, "No database connection. Run 'rails db:create' first.", :red
|
174
|
+
false
|
175
|
+
end
|
176
|
+
|
177
|
+
def providers
|
178
|
+
options[:providers]
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class AddOauthToUsers < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
|
4
|
+
def change
|
5
|
+
# Add oauth_user flag to identify users created through OAuth
|
6
|
+
# This helps with password validation for has_secure_password
|
7
|
+
add_column :users, :oauth_user, :boolean, default: false
|
8
|
+
|
9
|
+
# These fields are added by default for better OAuth integration
|
10
|
+
# You can comment out any fields you don't want to use
|
11
|
+
|
12
|
+
# Cache the avatar URL from OAuth for quicker access
|
13
|
+
add_column :users, :avatar_url, :string, null: true
|
14
|
+
|
15
|
+
# Track when the user last authenticated via OAuth
|
16
|
+
add_column :users, :last_oauth_login_at, :datetime, null: true
|
17
|
+
|
18
|
+
# Track which provider was most recently used
|
19
|
+
add_column :users, :last_oauth_provider, :string, null: true
|
20
|
+
|
21
|
+
# Note: All OAuth identity information (tokens, credentials, etc.) is
|
22
|
+
# stored in the clavis_oauth_identities table, not directly on the User.
|
23
|
+
|
24
|
+
# If you have existing provider/uid columns, uncomment this to remove them:
|
25
|
+
remove_column :users, :provider, :string
|
26
|
+
remove_column :users, :uid, :string
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
/* Clavis OAuth Button Styles */
|
2
|
+
|
3
|
+
.clavis-oauth-button {
|
4
|
+
display: inline-flex;
|
5
|
+
align-items: center;
|
6
|
+
justify-content: center;
|
7
|
+
padding: 10px 16px;
|
8
|
+
border-radius: 4px;
|
9
|
+
font-size: 14px;
|
10
|
+
font-weight: 500;
|
11
|
+
text-decoration: none;
|
12
|
+
margin: 5px 0;
|
13
|
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
14
|
+
transition: all 0.2s ease;
|
15
|
+
cursor: pointer;
|
16
|
+
min-width: 240px;
|
17
|
+
height: 40px;
|
18
|
+
box-sizing: border-box;
|
19
|
+
}
|
20
|
+
|
21
|
+
.clavis-oauth-button__icon,
|
22
|
+
.clavis-oauth-button span .clavis-icon {
|
23
|
+
width: 18px;
|
24
|
+
height: 18px;
|
25
|
+
margin-right: 10px;
|
26
|
+
fill: currentColor;
|
27
|
+
}
|
28
|
+
|
29
|
+
.clavis-oauth-button span {
|
30
|
+
line-height: 1;
|
31
|
+
}
|
32
|
+
|
33
|
+
/* Google - Following Google branding guidelines */
|
34
|
+
.clavis-oauth-button--google {
|
35
|
+
background-color: white;
|
36
|
+
color: rgba(0, 0, 0, 0.54);
|
37
|
+
border: 1px solid #dadce0;
|
38
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
39
|
+
font-weight: 500;
|
40
|
+
}
|
41
|
+
|
42
|
+
.clavis-oauth-button--google:hover {
|
43
|
+
background-color: #f8f8f8;
|
44
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
45
|
+
}
|
46
|
+
|
47
|
+
.clavis-oauth-button--google:focus {
|
48
|
+
box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.3);
|
49
|
+
outline: none;
|
50
|
+
}
|
51
|
+
|
52
|
+
.clavis-oauth-button--google .clavis-icon {
|
53
|
+
width: 18px;
|
54
|
+
height: 18px;
|
55
|
+
}
|
56
|
+
|
57
|
+
/* GitHub */
|
58
|
+
.clavis-oauth-button--github {
|
59
|
+
background-color: #24292e;
|
60
|
+
color: white;
|
61
|
+
}
|
62
|
+
|
63
|
+
.clavis-oauth-button--github:hover {
|
64
|
+
background-color: #2c3238;
|
65
|
+
border-color: #24292e;
|
66
|
+
}
|
67
|
+
|
68
|
+
/* Apple - Following Apple's Sign in with Apple guidelines */
|
69
|
+
.clavis-oauth-button--apple {
|
70
|
+
background-color: black;
|
71
|
+
color: white;
|
72
|
+
border-radius: 4px;
|
73
|
+
}
|
74
|
+
|
75
|
+
.clavis-oauth-button--apple:hover {
|
76
|
+
background-color: #333;
|
77
|
+
}
|
78
|
+
|
79
|
+
.clavis-oauth-button--apple .clavis-icon {
|
80
|
+
width: 16px;
|
81
|
+
height: 16px;
|
82
|
+
}
|
83
|
+
|
84
|
+
/* Facebook - Following Facebook branding guidelines */
|
85
|
+
.clavis-oauth-button--facebook {
|
86
|
+
background-color: #1877F2;
|
87
|
+
color: white;
|
88
|
+
border: none;
|
89
|
+
font-weight: bold;
|
90
|
+
}
|
91
|
+
|
92
|
+
.clavis-oauth-button--facebook:hover {
|
93
|
+
background-color: #166fe5;
|
94
|
+
border-color: #166fe5;
|
95
|
+
}
|
96
|
+
|
97
|
+
/* Microsoft - Following Microsoft branding guidelines */
|
98
|
+
.clavis-oauth-button--microsoft {
|
99
|
+
background-color: white;
|
100
|
+
color: #5e5e5e;
|
101
|
+
border: 1px solid #8c8c8c;
|
102
|
+
}
|
103
|
+
|
104
|
+
.clavis-oauth-button--microsoft:hover {
|
105
|
+
background-color: #f0f0f0;
|
106
|
+
}
|
107
|
+
|
108
|
+
.clavis-oauth-button--microsoft .clavis-icon {
|
109
|
+
width: 16px;
|
110
|
+
height: 16px;
|
111
|
+
}
|
112
|
+
|
113
|
+
/* Generic OAuth button */
|
114
|
+
.clavis-oauth-button--oauth {
|
115
|
+
background-color: #f8f9fa;
|
116
|
+
color: #202124;
|
117
|
+
border: 1px solid #dadce0;
|
118
|
+
}
|
119
|
+
|
120
|
+
.clavis-oauth-button--oauth:hover {
|
121
|
+
background-color: #f1f3f4;
|
122
|
+
}
|
123
|
+
|
124
|
+
/* Error message */
|
125
|
+
.clavis-error {
|
126
|
+
color: #721c24;
|
127
|
+
background-color: #f8d7da;
|
128
|
+
border: 1px solid #f5c6cb;
|
129
|
+
padding: 10px;
|
130
|
+
border-radius: 4px;
|
131
|
+
margin: 10px 0;
|
132
|
+
font-size: 14px;
|
133
|
+
}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Clavis.configure do |config|
|
4
|
+
# Configure your OAuth providers here
|
5
|
+
config.providers = {
|
6
|
+
<% providers.each do |provider| %>
|
7
|
+
<%= provider %>: {
|
8
|
+
client_id: ENV["<%= provider.upcase %>_CLIENT_ID"] || Rails.application.credentials.dig(:<%= provider %>, :client_id),
|
9
|
+
client_secret: ENV["<%= provider.upcase %>_CLIENT_SECRET"] || Rails.application.credentials.dig(:<%= provider %>, :client_secret),
|
10
|
+
# IMPORTANT: This exact URI must be registered in the <%= provider.capitalize %> developer console/dashboard
|
11
|
+
# For example, in Google Cloud Console: APIs & Services > Credentials > OAuth 2.0 Client IDs > Authorized redirect URIs
|
12
|
+
redirect_uri: "http://localhost:3000/auth/<%= provider %>/callback" # Change this in production
|
13
|
+
},
|
14
|
+
<% end %>
|
15
|
+
}
|
16
|
+
|
17
|
+
# Default scopes to request from providers
|
18
|
+
# config.default_scopes = "email profile"
|
19
|
+
|
20
|
+
# Enable verbose logging for debugging
|
21
|
+
# config.verbose_logging = true
|
22
|
+
|
23
|
+
# User class and finder method
|
24
|
+
# These settings control how Clavis finds or creates users from OAuth data
|
25
|
+
# config.user_class = "User" # The class to use for user creation/lookup
|
26
|
+
# config.user_finder_method = :find_or_create_from_clavis # The method to call on user_class
|
27
|
+
#
|
28
|
+
# Make sure to add this method to your User model:
|
29
|
+
# rails generate clavis:user_method
|
30
|
+
#
|
31
|
+
# Or implement it manually with your custom logic
|
32
|
+
|
33
|
+
# Custom claims processor
|
34
|
+
# config.claims_processor = proc do |auth_hash, user|
|
35
|
+
# # Process specific claims
|
36
|
+
# if auth_hash[:provider] == "google" && auth_hash[:info][:email_verified]
|
37
|
+
# user.verified_email = true
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
|
41
|
+
# IMPORTANT: By default, after successful authentication users will be
|
42
|
+
# redirected to your application's root_path. If you need to customize this,
|
43
|
+
# you can override the after_login_path method in your own controller.
|
44
|
+
#
|
45
|
+
# If you're experiencing redirect loops after authentication, make sure
|
46
|
+
# you have a root_path defined in your application.
|
47
|
+
end
|