diff --git a/.env.sample b/.env.sample index 808bc653..b3c45593 100644 --- a/.env.sample +++ b/.env.sample @@ -5,4 +5,9 @@ # web-push generate-vapid-keys # ``` VAPID_PUBLIC_KEY= -VAPID_PRIVATE_KEY= \ No newline at end of file +VAPID_PRIVATE_KEY= + +# Twitter OAuth credentials +# Get these from https://developer.twitter.com/en/portal/projects-and-apps +TWITTER_CLIENT_ID= +TWITTER_CLIENT_SECRET= \ No newline at end of file diff --git a/Gemfile b/Gemfile index 716bfdd3..ca6160a4 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,11 @@ gem "image_processing", ">= 1.2" gem "sentry-ruby" gem "sentry-rails" +# OAuth +gem "omniauth" +gem "omniauth-twitter2" +gem "omniauth-rails_csrf_protection" + # Other gem "bcrypt" gem "web-push" diff --git a/TWITTER_OAUTH_SETUP.md b/TWITTER_OAUTH_SETUP.md new file mode 100644 index 00000000..5d13dd6a --- /dev/null +++ b/TWITTER_OAUTH_SETUP.md @@ -0,0 +1,78 @@ +# Twitter OAuth Setup Guide + +This guide explains how to set up Twitter OAuth integration for Small Bets. + +## Features + +- **Sign in with X**: Users can sign in using their X (Twitter) account +- **Account Linking**: Existing users can link their X account to their profile +- **Automatic Profile Updates**: When linking, the user's Twitter username and profile URL are automatically updated + +## Setup Instructions + +### 1. Create Twitter Developer App + +1. Go to [Twitter Developer Portal](https://developer.twitter.com/en/portal/projects-and-apps) +2. Create a new project and app +3. In your app settings, go to "User authentication settings" +4. Configure OAuth 2.0 settings: + - App permissions: Read + - Type of App: Web App + - Callback URLs: `https://your-domain.com/auth/twitter2/callback` + - Website URL: `https://your-domain.com` + +### 2. Environment Variables + +Add the following to your `.env` file: + +``` +TWITTER_CLIENT_ID=your_client_id_here +TWITTER_CLIENT_SECRET=your_client_secret_here +``` + +### 3. Install Dependencies + +Run `bundle install` to install the required gems: +- `omniauth` +- `omniauth-twitter2` +- `omniauth-rails_csrf_protection` + +### 4. Database Migration + +Run the migration to add Twitter OAuth fields: + +```bash +rails db:migrate +``` + +## Usage + +### Sign In Page +- Users can click "Continue with X" on the sign-in page +- If they have an account with a matching email, it will be linked +- If not, a new account will be created (if email is provided) + +### Profile Page +- Users can connect their X account from their profile page +- Once connected, their Twitter username will be displayed +- The Twitter URL field will be automatically populated + +## Technical Details + +### Database Changes +- Added `twitter_uid` (string, unique index) +- Added `twitter_username` (string) +- Existing `twitter_url` field is reused for profile links + +### OAuth Flow +1. User clicks "Continue with X" +2. Redirected to Twitter OAuth +3. After authorization, redirected to `/auth/twitter2/callback` +4. `OauthCallbacksController` handles the response +5. User is either signed in or their account is linked + +### Security +- Uses OAuth 2.0 with PKCE +- CSRF protection enabled +- Unique constraints on Twitter UID +- Proper error handling for failed authentications \ No newline at end of file diff --git a/app/controllers/oauth_callbacks_controller.rb b/app/controllers/oauth_callbacks_controller.rb new file mode 100644 index 00000000..ba78a388 --- /dev/null +++ b/app/controllers/oauth_callbacks_controller.rb @@ -0,0 +1,76 @@ +class OauthCallbacksController < ApplicationController + allow_unauthenticated_access only: %i[ create failure ] + + def create + auth = request.env["omniauth.auth"] + + case auth.provider + when "twitter2" + handle_twitter_auth(auth) + else + redirect_to new_session_path, alert: "Authentication provider not supported." + end + end + + def failure + redirect_to new_session_path, alert: "Authentication failed. Please try again." + end + + private + + def handle_twitter_auth(auth) + email = auth.info.email + twitter_uid = auth.uid + twitter_username = auth.info.nickname + name = auth.info.name + + if user_signed_in? + # User is already signed in, connect Twitter account + Current.user.update!( + twitter_uid: twitter_uid, + twitter_username: twitter_username, + twitter_url: "https://x.com/#{twitter_username}" + ) + redirect_to user_profile_path, notice: "X account connected successfully!" + else + # Try to find user by Twitter UID first + user = User.find_by(twitter_uid: twitter_uid) + + # If not found and email is present, try to find by email + user ||= User.find_by(email_address: email) if email.present? + + if user + # Update Twitter info if user exists + user.update!( + twitter_uid: twitter_uid, + twitter_username: twitter_username, + twitter_url: "https://x.com/#{twitter_username}" + ) + authenticate_user(user) + redirect_to root_path + else + # Create new user if email is present + if email.present? + user = User.create!( + email_address: email, + name: name.presence || User::DEFAULT_NAME, + twitter_uid: twitter_uid, + twitter_username: twitter_username, + twitter_url: "https://x.com/#{twitter_username}" + ) + authenticate_user(user) + redirect_to root_path + else + redirect_to new_session_path, alert: "Email address is required for registration." + end + end + end + rescue ActiveRecord::RecordInvalid => e + redirect_to new_session_path, alert: "Registration failed: #{e.message}" + end + + def authenticate_user(user) + session = user.sessions.create! + cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax } + end +end \ No newline at end of file diff --git a/app/controllers/users/profiles_controller.rb b/app/controllers/users/profiles_controller.rb index 16be9526..7189c31e 100644 --- a/app/controllers/users/profiles_controller.rb +++ b/app/controllers/users/profiles_controller.rb @@ -16,7 +16,7 @@ def set_user end def user_params - params.require(:user).permit(:name, :avatar, :email_address, :password, :bio, :twitter_url, :linkedin_url, :personal_url).compact + params.require(:user).permit(:name, :avatar, :email_address, :password, :bio, :twitter_url, :linkedin_url, :personal_url, :twitter_uid, :twitter_username).compact end def after_update_url diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index a488eda2..03a482c5 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -14,11 +14,11 @@ <%= form_with url: auth_tokens_url, class: "flex flex-column gap", data: { turbo: hotwire_native_app? }, html: { autocomplete: "off" } do |form| %>