-
Notifications
You must be signed in to change notification settings - Fork 102
Feature: Linking X/Twitter Account in the Profile and Logging in via X oauth #123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
e1d2028
95c5910
7eef3ba
bee4ac4
2652d93
c406588
224abbd
11f2c49
54fe514
328a0e2
483c71d
75a96b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| class Users::OmniauthCallbacksController < ApplicationController | ||
| include Authentication | ||
|
|
||
| skip_before_action :verify_authenticity_token, only: [ :twitter, :failure ] | ||
| skip_before_action :require_authentication, only: [ :twitter, :failure, :initiate_login ] | ||
| before_action :restore_authentication, only: [ :twitter ] | ||
|
Comment on lines
+4
to
+6
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line prevents redirect during OAuth callback while still restoring session |
||
|
|
||
| def twitter | ||
| auth_hash = request.env["omniauth.auth"] | ||
| login_only = session.delete(:oauth_login_only) | ||
|
Comment on lines
+9
to
+10
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line flags login vs connect flow. |
||
|
|
||
| if auth_hash.blank? | ||
| redirect_to new_session_path, alert: "Authentication failed. Please try again." | ||
| return | ||
| end | ||
|
|
||
| if login_only | ||
| handle_login(auth_hash) | ||
| else | ||
| handle_connect(auth_hash) | ||
| end | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def handle_login(auth_hash) | ||
| user = User.find_by(twitter_uid: auth_hash["uid"]) | ||
|
|
||
| if user.present? | ||
| start_new_session_for(user) | ||
| redirect_to root_path, notice: "Signed in with X successfully!" | ||
| else | ||
| redirect_to new_session_path, alert: "No account found with this X account. Please sign in with email first and connect your X account." | ||
| end | ||
| end | ||
|
Comment on lines
+26
to
+35
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| def handle_connect(auth_hash) | ||
| unless Current.user | ||
| redirect_to new_session_path, alert: "Please sign in first to connect your X account." | ||
| return | ||
| end | ||
|
|
||
| begin | ||
| Current.user.update!( | ||
| twitter_uid: auth_hash["uid"], | ||
| twitter_oauth_token: auth_hash.dig("credentials", "token"), | ||
| twitter_oauth_refresh_token: auth_hash.dig("credentials", "refresh_token"), | ||
| twitter_screen_name: auth_hash.dig("info", "nickname"), | ||
| twitter_username: auth_hash.dig("info", "nickname"), | ||
| twitter_profile_image: auth_hash.dig("info", "image"), | ||
| twitter_connected_at: Time.current, | ||
| twitter_url: "https://x.com/#{auth_hash.dig('info', 'nickname')}" | ||
| ) | ||
|
|
||
| redirect_to user_profile_path("me"), notice: "Successfully connected your X account!" | ||
| rescue => e | ||
| Rails.logger.error "X OAuth Error: #{e.message}" | ||
| Rails.logger.error e.backtrace.join("\n") | ||
| redirect_to user_profile_path("me"), alert: "Failed to connect X account. Please try again." | ||
| end | ||
| end | ||
|
Comment on lines
+37
to
+61
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updates current user with OAuth data (requires authentication). |
||
|
|
||
| public | ||
|
|
||
| def initiate_login | ||
| session[:oauth_login_only] = true | ||
| render inline: <<-HTML, layout: false | ||
| <!DOCTYPE html> | ||
| <html> | ||
| <body> | ||
| <form id="oauth-form" action="/auth/twitter2" method="post"> | ||
| <input type="hidden" name="authenticity_token" value="#{form_authenticity_token}"> | ||
| </form> | ||
| <script>document.getElementById('oauth-form').submit();</script> | ||
| </body> | ||
| </html> | ||
| HTML | ||
| end | ||
|
Comment on lines
+65
to
+78
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Auto-submits POST form to start OAuth (required by CSRF protection) |
||
|
|
||
| def failure | ||
| error_message = request.params[:message] || "Authentication failed" | ||
| Rails.logger.error "OmniAuth Failure: #{error_message}" | ||
| redirect_to user_profile_path("me"), alert: "X authentication failed: #{error_message}" | ||
| end | ||
|
|
||
| def disconnect | ||
| Current.user.update!( | ||
| twitter_uid: nil, | ||
| twitter_oauth_token: nil, | ||
| twitter_oauth_refresh_token: nil, | ||
| twitter_screen_name: nil, | ||
| twitter_username: nil, | ||
| twitter_profile_image: nil, | ||
| twitter_connected_at: nil, | ||
| twitter_url: nil | ||
| ) | ||
|
|
||
| redirect_to user_profile_path("me"), notice: "X account disconnected successfully!" | ||
| end | ||
|
|
||
|
Comment on lines
+86
to
+100
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clears all db fields and disconnects the user's X account. |
||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,7 @@ def mentioning_messages | |
|
|
||
| validates_presence_of :email_address, if: :person? | ||
| normalizes :email_address, with: ->(email_address) { email_address.downcase } | ||
| validates :twitter_uid, uniqueness: true, allow_nil: true | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prevents one X account from being linked to multiple users. |
||
|
|
||
| scope :without_default_names, -> { where.not(name: DEFAULT_NAME) } | ||
| scope :non_suspended, -> { where(suspended_at: nil) } | ||
|
|
@@ -215,6 +216,10 @@ def unblock!(other_user) | |
| blocks_given.where(blocked: other_user).destroy_all | ||
| end | ||
|
|
||
| def twitter_connected? | ||
| twitter_uid.present? && twitter_connected_at.present? | ||
| end | ||
|
|
||
|
Comment on lines
+219
to
+222
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Helper to check if account is linked. This is only used in the test to make sure that the X account is linked properly. |
||
| private | ||
| def self.find_and_initialize_unclaimed_gumroad_import(attributes) | ||
| unclaimed_gumroad_import = User.active.non_suspended.unclaimed_gumroad_imports.find_by(email_address: attributes[:email_address]) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,22 @@ | |
| <% end %> | ||
| </fieldset> | ||
| <% end %> | ||
|
|
||
| <div class="flex align-center gap center-block upad" style="margin-top: 0.5rem;"> | ||
| <hr class="flex-item-grow borderless" style="border-top: 1px solid var(--color-border);"> | ||
| <span class="txt-small txt-deemphasized">or</span> | ||
| <hr class="flex-item-grow borderless" style="border-top: 1px solid var(--color-border);"> | ||
| </div> | ||
|
|
||
| <div class="flex align-center gap center-block upad" style="margin-top: 0; justify-content: center;"> | ||
| <form id="x-login-form" action="/auth/twitter2/login" method="post" data-turbo="false" style="width: 100%;"> | ||
| <input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>"> | ||
| <button type="submit" class="flex align-center gap input input--actor txt-large" style="width: 100%; cursor: pointer; justify-content: center;"> | ||
| <span>Sign in with</span> | ||
| <%= image_tag "social/twitter-outline.svg", role: "presentation", size: 20, class: "colorize--black" %> | ||
| </button> | ||
| </form> | ||
| </div> | ||
|
Comment on lines
+41
to
+55
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added "Login with X" button that links to initiate_login endpoint. |
||
| </div> | ||
| </div> | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -79,10 +79,18 @@ | |
| <div class="flex align-center gap"> | ||
| <%= translation_button(:twitter) %> | ||
|
|
||
| <label class="flex align-center gap flex-item-grow input input--actor"> | ||
| <%= form.text_field :twitter_url, class: "input txt-medium ", placeholder: "Your X profile", required: false %> | ||
| <%= image_tag "social/twitter-outline.svg", role: "presentation", size: 24, class: "colorize--black" %> | ||
| </label> | ||
| <% if @user.twitter_connected_at.present? %> | ||
| <label class="flex align-center gap flex-item-grow input input--actor"> | ||
| <%= link_to "@#{@user.twitter_screen_name}", @user.twitter_url, target: "_blank", class: "input txt-medium", style: "text-decoration: none; color: inherit;" %> | ||
| <%= image_tag "social/twitter-outline.svg", role: "presentation", size: 24, class: "colorize--black" %> | ||
| </label> | ||
| <%= link_to "Disconnect", "/auth/twitter/disconnect", data: { turbo_method: :delete, turbo_confirm: "Disconnect your X account?" }, class: "btn txt-small" %> | ||
| <% else %> | ||
| <a href="#" onclick="document.getElementById('x-oauth-form').submit(); return false;" class="flex align-center gap flex-item-grow input input--actor" style="cursor: pointer;"> | ||
| <span class="input txt-medium txt-deemphasized">Connect your X account</span> | ||
| <%= image_tag "social/twitter-outline.svg", role: "presentation", size: 24, class: "colorize--black" %> | ||
| </a> | ||
| <% end %> | ||
|
Comment on lines
+82
to
+93
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After account is linked, it will show users a clickable button with their X account username that redirects them to X for authorization. After account linking, the disconnect button appears next to the X account. |
||
| </div> | ||
|
|
||
| <div class="flex align-center gap"> | ||
|
|
@@ -163,4 +171,10 @@ | |
| <%= link_to "Push Notifications Dev Mode", user_push_subscriptions_url, class: "btn txt-small center" %> | ||
| </fieldset> | ||
| <% end %> | ||
|
|
||
| <% unless @user.twitter_connected_at.present? %> | ||
| <form id="x-oauth-form" action="/auth/twitter2" method="post" style="display: none;"> | ||
| <input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>"> | ||
| </form> | ||
| <% end %> | ||
|
Comment on lines
+174
to
+179
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the form where it initiates the X account linking by calling the auth/twitter2 endpoint. |
||
| </section> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| Rails.application.config.middleware.use OmniAuth::Builder do | ||
| provider :twitter2, | ||
| ENV["X_CLIENT_ID"], | ||
| ENV["X_CLIENT_SECRET"], | ||
| scope: "tweet.read users.read offline.access" | ||
| end | ||
|
Comment on lines
+1
to
+6
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Configures OmniAuth middleware with X client ID/secret and required scopes. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -71,6 +71,11 @@ | |
| resource :email_subscription, only: %i[ show update ] | ||
| end | ||
|
|
||
| get "/auth/:provider/callback", to: "users/omniauth_callbacks#twitter" | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| get "/auth/failure", to: "users/omniauth_callbacks#failure" | ||
| delete "/auth/twitter/disconnect", to: "users/omniauth_callbacks#disconnect" | ||
|
Comment on lines
+75
to
+76
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These two endpoints handle auth failures and account disconnection. |
||
| post "/auth/twitter2/login", to: "users/omniauth_callbacks#initiate_login" | ||
|
|
||
|
Comment on lines
+77
to
+78
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This endpoint is used for auth login using Twitter Oauth 2.0. |
||
| resources :users, only: :show do | ||
| scope module: "users" do | ||
| resource :avatar, only: %i[ show destroy ] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| class AddTwitterOAuthToUsers < ActiveRecord::Migration[7.2] | ||
| def change | ||
| add_column :users, :twitter_uid, :string | ||
| add_column :users, :twitter_oauth_token, :text | ||
| add_column :users, :twitter_oauth_refresh_token, :text | ||
| add_column :users, :twitter_screen_name, :string | ||
| add_column :users, :twitter_profile_image, :string | ||
| add_column :users, :twitter_connected_at, :datetime | ||
|
|
||
| add_index :users, :twitter_uid, unique: true | ||
| end | ||
| end | ||
|
Comment on lines
+1
to
+12
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adds OAuth db fields to User table. This will be used for refreshing the session and logging in during X OAuth flow. |
||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These three gems are recommended on X documentation page here.
omniauth-twitter2is dependent onomniauth.omniauth-rails_csrf_protectionadds CSRF protection toomniauth.