From a4dcd0387fe52b4de7fef4621c571299aa4e40d8 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Fri, 14 Nov 2025 18:39:15 +0100 Subject: [PATCH] Remove patreon OAuth integration --- app/controllers/settings_controller.rb | 8 - .../users/omniauth_callbacks_controller.rb | 4 - app/models/user.rb | 54 +---- app/services/patreon/patron_checker.rb | 131 ------------ app/views/settings/index.html.erb | 31 +-- config/initializers/01_constants.rb | 5 +- config/initializers/devise.rb | 6 - config/initializers/omniauth.rb | 2 +- config/routes.rb | 1 - ...251028160950_add_patreon_token_to_users.rb | 7 - docs/PATREON_INTEGRATION.md | 194 ------------------ lib/omniauth/strategies/patreon.rb | 46 ----- 12 files changed, 6 insertions(+), 483 deletions(-) delete mode 100644 app/services/patreon/patron_checker.rb delete mode 100644 db/migrate/20251028160950_add_patreon_token_to_users.rb delete mode 100644 docs/PATREON_INTEGRATION.md delete mode 100644 lib/omniauth/strategies/patreon.rb diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index d1f78c03..1a34fed4 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -28,14 +28,6 @@ class SettingsController < ApplicationController redirect_back(fallback_location: root_path) end - def disconnect_patreon - if current_user.disconnect_patreon! - redirect_to settings_path, notice: 'Patreon account disconnected successfully' - else - redirect_to settings_path, alert: 'Unable to disconnect Patreon account' - end - end - private def settings_params diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index e56dfd5e..10fd37ac 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -13,10 +13,6 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController handle_auth('OpenID Connect') end - def patreon - handle_auth('Patreon') - end - def failure error_type = request.env['omniauth.error.type'] error = request.env['omniauth.error'] diff --git a/app/models/user.rb b/app/models/user.rb index a4390ce2..8a44e379 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -156,10 +156,7 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength user = User.find_by(provider: provider, uid: uid) if user - # Update tokens for existing user - if provider == 'patreon' - user.update_patreon_tokens(access_token) - end + # User found by provider/uid return user end @@ -169,9 +166,6 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength if user # Update provider and uid for existing user (first-time linking) user.update(provider: provider, uid: uid) - if provider == 'patreon' - user.update_patreon_tokens(access_token) - end return user end @@ -183,55 +177,9 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength uid: uid ) - if provider == 'patreon' - user.update_patreon_tokens(access_token) - end - user end - def patreon_connected? - provider == 'patreon' && uid.present? - end - - def disconnect_patreon! - return false unless patreon_connected? - - update( - provider: nil, - uid: nil, - patreon_access_token: nil, - patreon_refresh_token: nil, - patreon_token_expires_at: nil - ) - end - - def update_patreon_tokens(auth) - credentials = auth.credentials - update( - patreon_access_token: credentials.token, - patreon_refresh_token: credentials.refresh_token, - patreon_token_expires_at: Time.at(credentials.expires_at) - ) - end - - # Check if user is a patron of a specific creator - # @param creator_id [String] The Patreon creator ID to check - # @return [Boolean] true if user is an active patron - def patron_of?(creator_id) - return false unless patreon_connected? - - Patreon::PatronChecker.new(self).patron_of?(creator_id) - end - - # Get all campaigns the user is supporting - # @return [Array] Array of campaign data - def patreon_memberships - return [] unless patreon_connected? - - Patreon::PatronChecker.new(self).memberships - end - private def create_api_key diff --git a/app/services/patreon/patron_checker.rb b/app/services/patreon/patron_checker.rb deleted file mode 100644 index 575f83f6..00000000 --- a/app/services/patreon/patron_checker.rb +++ /dev/null @@ -1,131 +0,0 @@ -# frozen_string_literal: true - -module Patreon - # Service to check Patreon patron status - class PatronChecker - attr_reader :user - - def initialize(user) - @user = user - end - - # Check if user is a patron of a specific creator - # @param creator_id [String] The Patreon creator ID - # @return [Boolean] true if user is an active patron - def patron_of?(creator_id) - memberships.any? do |membership| - membership.dig('relationships', 'campaign', 'data', 'id') == creator_id.to_s && - membership.dig('attributes', 'patron_status') == 'active_patron' - end - end - - # Get all active memberships - # @return [Array] Array of membership data with campaign info - def memberships - @memberships ||= fetch_memberships - end - - # Get detailed membership info for a specific creator - # @param creator_id [String] The Patreon creator ID - # @return [Hash, nil] Membership details or nil if not a patron - def membership_for(creator_id) - memberships.find do |membership| - membership.dig('relationships', 'campaign', 'data', 'id') == creator_id.to_s - end - end - - private - - def fetch_memberships - return [] unless valid_token? - - response = make_api_request - return [] unless response - - extract_memberships(response) - rescue StandardError => e - Rails.logger.error("Failed to fetch Patreon memberships: #{e.message}") - [] - end - - def valid_token? - return false if user.patreon_access_token.blank? - - # Check if token is expired - if user.patreon_token_expires_at && user.patreon_token_expires_at < Time.current - refresh_token! - end - - user.patreon_access_token.present? - end - - def refresh_token! - return false if user.patreon_refresh_token.blank? - - conn = Faraday.new(url: 'https://www.patreon.com') do |f| - f.request :url_encoded - f.response :json - f.adapter Faraday.default_adapter - end - - response = conn.post('/api/oauth2/token') do |req| - req.body = { - grant_type: 'refresh_token', - refresh_token: user.patreon_refresh_token, - client_id: ENV['PATREON_CLIENT_ID'], - client_secret: ENV['PATREON_CLIENT_SECRET'] - } - end - - if response.success? - data = response.body - user.update( - patreon_access_token: data['access_token'], - patreon_refresh_token: data['refresh_token'], - patreon_token_expires_at: Time.current + data['expires_in'].seconds - ) - true - else - Rails.logger.error("Failed to refresh Patreon token: #{response.body}") - false - end - rescue StandardError => e - Rails.logger.error("Error refreshing Patreon token: #{e.message}") - false - end - - def make_api_request - conn = Faraday.new(url: 'https://www.patreon.com') do |f| - f.request :url_encoded - f.response :json - f.adapter Faraday.default_adapter - end - - response = conn.get('/api/oauth2/v2/identity') do |req| - req.headers['Authorization'] = "Bearer #{user.patreon_access_token}" - req.params = { - include: 'memberships,memberships.campaign', - 'fields[member]' => 'patron_status,pledge_relationship_start', - 'fields[campaign]' => 'vanity,url' - } - end - - response.success? ? response.body : nil - end - - def extract_memberships(response) - return [] unless response['included'] - - memberships = response['included'].select { |item| item['type'] == 'member' } - campaigns = response['included'].select { |item| item['type'] == 'campaign' } - - # Enrich memberships with campaign data - memberships.map do |membership| - campaign_id = membership.dig('relationships', 'campaign', 'data', 'id') - campaign = campaigns.find { |c| c['id'] == campaign_id } - - membership.merge('campaign' => campaign) if campaign - end.compact - end - end -end diff --git a/app/views/settings/index.html.erb b/app/views/settings/index.html.erb index 85ac469f..7b412fa8 100644 --- a/app/views/settings/index.html.erb +++ b/app/views/settings/index.html.erb @@ -78,35 +78,8 @@ Connected Accounts
-
-
- - - -
-

Patreon

-

- <% if current_user.patreon_connected? %> - Connected - <% else %> - Not connected - <% end %> -

-
-
- <% if current_user.patreon_connected? %> - <%= button_to "Disconnect", disconnect_patreon_settings_path, - method: :delete, - class: "btn btn-sm btn-outline btn-error", - form: { data: { turbo_confirm: "Are you sure you want to disconnect your Patreon account?" } } %> - <% else %> - <%= link_to "Connect", user_patreon_omniauth_authorize_path, - class: "btn btn-sm btn-primary", - method: :post %> - <% end %> -
-

- Connect your Patreon account to support the development of Dawarich and get access to exclusive features. +

+ OAuth providers configured: <%= OMNIAUTH_PROVIDERS.map(&:to_s).join(', ').presence || 'None' %>

diff --git a/config/initializers/01_constants.rb b/config/initializers/01_constants.rb index 8bb2de8e..0215de24 100644 --- a/config/initializers/01_constants.rb +++ b/config/initializers/01_constants.rb @@ -38,16 +38,15 @@ METRICS_PASSWORD = ENV.fetch('METRICS_PASSWORD', 'prometheus') # /Prometheus metrics # Configure OAuth providers based on environment -# Self-hosted: only OpenID Connect, Cloud: only GitHub, Google, and Patreon +# Self-hosted: only OpenID Connect, Cloud: only GitHub and Google OMNIAUTH_PROVIDERS = if SELF_HOSTED # Self-hosted: only OpenID Connect ENV['OIDC_CLIENT_ID'].present? ? %i[openid_connect] : [] else - # Cloud: only GitHub, Google, and Patreon + # Cloud: only GitHub and Google providers = [] providers << :github if ENV['GITHUB_OAUTH_CLIENT_ID'].present? providers << :google_oauth2 if ENV['GOOGLE_OAUTH_CLIENT_ID'].present? - providers << :patreon if ENV['PATREON_CLIENT_ID'].present? providers end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 5fd04c74..2deaf387 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -278,12 +278,6 @@ Devise.setup do |config| scope: 'userinfo.email,userinfo.profile' Rails.logger.info 'OAuth: Google configured' end - - if ENV['PATREON_CLIENT_ID'].present? && ENV['PATREON_CLIENT_SECRET'].present? - config.omniauth :patreon, ENV['PATREON_CLIENT_ID'], ENV['PATREON_CLIENT_SECRET'], - scope: 'identity identity[email]' - Rails.logger.info 'OAuth: Patreon configured' - end end # Self-hosted version: only OpenID Connect (when env vars present) diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index ae8c8277..aa158eb6 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true # Load custom OmniAuth strategies -require Rails.root.join('lib', 'omniauth', 'strategies', 'patreon') +# (none currently needed) diff --git a/config/routes.rb b/config/routes.rb index bd650203..16d2eacb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -50,7 +50,6 @@ Rails.application.routes.draw do patch 'settings', to: 'settings#update' get 'settings/theme', to: 'settings#theme' post 'settings/generate_api_key', to: 'settings#generate_api_key', as: :generate_api_key - delete 'settings/disconnect_patreon', to: 'settings#disconnect_patreon', as: :disconnect_patreon_settings resources :imports resources :visits, only: %i[index update] diff --git a/db/migrate/20251028160950_add_patreon_token_to_users.rb b/db/migrate/20251028160950_add_patreon_token_to_users.rb deleted file mode 100644 index 77cd928f..00000000 --- a/db/migrate/20251028160950_add_patreon_token_to_users.rb +++ /dev/null @@ -1,7 +0,0 @@ -class AddPatreonTokenToUsers < ActiveRecord::Migration[8.0] - def change - add_column :users, :patreon_access_token, :text - add_column :users, :patreon_refresh_token, :text - add_column :users, :patreon_token_expires_at, :datetime - end -end diff --git a/docs/PATREON_INTEGRATION.md b/docs/PATREON_INTEGRATION.md deleted file mode 100644 index dd35e938..00000000 --- a/docs/PATREON_INTEGRATION.md +++ /dev/null @@ -1,194 +0,0 @@ -# Patreon Integration - -Dawarich Cloud includes Patreon OAuth integration that allows users to connect their Patreon accounts. This enables checking if users are patrons of specific creators. - -## Features - -- **OAuth Authentication**: Users can connect their Patreon accounts via OAuth 2.0 -- **Patron Status Checking**: Check if a user is an active patron of specific creators -- **Membership Data**: Access detailed information about user's Patreon memberships -- **Token Management**: Automatic token refresh to maintain API access - -## Setup - -### Environment Variables - -Configure the following environment variables for Dawarich Cloud: - -```bash -PATREON_CLIENT_ID=your_patreon_client_id -PATREON_CLIENT_SECRET=your_patreon_client_secret -``` - -### Getting Patreon OAuth Credentials - -1. Go to [Patreon Developer Portal](https://www.patreon.com/portal/registration/register-clients) -2. Create a new OAuth client -3. Set the redirect URI to: `https://your-domain.com/users/auth/patreon/callback` -4. Copy the Client ID and Client Secret - -## Usage - -### User Connection - -Users can connect their Patreon account in the account settings: - -1. Navigate to Settings -2. Find the "Connected Accounts" section -3. Click "Connect" next to Patreon -4. Authorize the application on Patreon -5. Get redirected back to Dawarich - -### Checking Patron Status - -#### Check if User is a Patron of Specific Creator - -```ruby -# Get Dawarich creator's Patreon ID (find it in your Patreon campaign URL) -dawarich_creator_id = 'your_creator_id' - -# Check if current user is a patron -if current_user.patron_of?(dawarich_creator_id) - # User is an active patron! - # Grant special features, show badge, etc. -end -``` - -#### Get All Memberships - -```ruby -# Get all campaigns the user is supporting -memberships = current_user.patreon_memberships - -memberships.each do |membership| - campaign = membership['campaign'] - - puts "Supporting: #{campaign['attributes']['vanity']}" - puts "URL: #{campaign['attributes']['url']}" - puts "Status: #{membership['attributes']['patron_status']}" - puts "Since: #{membership['attributes']['pledge_relationship_start']}" -end -``` - -#### Get Specific Membership Details - -```ruby -creator_id = 'your_creator_id' - -membership = Patreon::PatronChecker.new(current_user).membership_for(creator_id) - -if membership - # User is a patron - status = membership.dig('attributes', 'patron_status') - started_at = membership.dig('attributes', 'pledge_relationship_start') - - # Access campaign details - campaign = membership['campaign'] - campaign_name = campaign.dig('attributes', 'vanity') -end -``` - -### Patron Status Values - -The `patron_status` field can have the following values: - -- `active_patron` - Currently an active patron -- `declined_patron` - Payment declined -- `former_patron` - Was a patron but not anymore - -### Example: Show Patron Badge - -```ruby -# In a view or helper -def show_patron_badge?(user) - dawarich_creator_id = ENV['DAWARICH_PATREON_CREATOR_ID'] - return false unless dawarich_creator_id.present? - - user.patron_of?(dawarich_creator_id) -end -``` - -```erb - -<% if show_patron_badge?(current_user) %> - - ❤️ Patreon Supporter - -<% end %> -``` - -### Example: Grant Premium Features - -```ruby -class User < ApplicationRecord - def premium_access? - return true if admin? - return true if active_subscription? # existing subscription logic - - # Check Patreon support - dawarich_creator_id = ENV['DAWARICH_PATREON_CREATOR_ID'] - return false unless dawarich_creator_id - - patron_of?(dawarich_creator_id) - end -end -``` - -## Token Management - -The integration automatically handles token refresh: - -- Access tokens are stored securely in the database -- Tokens are automatically refreshed when expired -- If refresh fails, the user needs to reconnect their account - -## Disconnecting Patreon - -Users can disconnect their Patreon account at any time: - -```ruby -current_user.disconnect_patreon! -``` - -This will: -- Remove the provider/uid linkage -- Clear all stored tokens -- Revoke API access - -## Security Considerations - -- Access tokens are stored in the database (consider encrypting at rest) -- Tokens are automatically refreshed to maintain access -- API requests are made server-side only -- Users can revoke access at any time from their Patreon settings - -## API Rate Limits - -Patreon API has rate limits. The service handles this by: -- Caching membership data when possible -- Using efficient API queries -- Handling API errors gracefully - -## Troubleshooting - -### Token Expired Errors - -If users see authentication errors: -1. Ask them to disconnect and reconnect their Patreon account -2. Check that the refresh token is still valid -3. Verify environment variables are set correctly - -### Creator ID Not Found - -To find a Patreon creator ID: -1. Go to the creator's Patreon page -2. Use the Patreon API: `GET https://www.patreon.com/api/oauth2/v2/campaigns` -3. The campaign ID is the creator ID you need - -## Future Enhancements - -Potential future features: -- Tier-based access (check pledge amount) -- Lifetime pledge amount tracking -- Patron anniversary badges -- Direct campaign data caching diff --git a/lib/omniauth/strategies/patreon.rb b/lib/omniauth/strategies/patreon.rb deleted file mode 100644 index f6103e3e..00000000 --- a/lib/omniauth/strategies/patreon.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'omniauth-oauth2' - -module OmniAuth - module Strategies - # OmniAuth strategy for Patreon OAuth2 - class Patreon < OmniAuth::Strategies::OAuth2 - option :name, 'patreon' - - option :client_options, - site: 'https://www.patreon.com', - authorize_url: 'https://www.patreon.com/oauth2/authorize', - token_url: 'https://www.patreon.com/api/oauth2/token' - - option :authorize_params, - scope: 'identity identity[email]' - - uid { raw_info['data']['id'] } - - info do - { - email: raw_info.dig('data', 'attributes', 'email'), - name: raw_info.dig('data', 'attributes', 'full_name'), - first_name: raw_info.dig('data', 'attributes', 'first_name'), - last_name: raw_info.dig('data', 'attributes', 'last_name'), - image: raw_info.dig('data', 'attributes', 'image_url') - } - end - - extra do - { - raw_info: raw_info - } - end - - def raw_info - @raw_info ||= access_token.get('/api/oauth2/v2/identity?include=memberships&fields[user]=email,first_name,full_name,last_name,image_url').parsed - end - - def callback_url - full_host + callback_path - end - end - end -end