From b574d57a80264ebc8e657a99dcbed81b98f4541d Mon Sep 17 00:00:00 2001 From: 24c02 <163450896+24c02@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:42:55 -0500 Subject: [PATCH] Add HCB OAuth and payment account controllers - OAuth flow for linking HCB accounts - CRUD for payment accounts (user/org pairs) - Routes under /back_office/hcb/ - Use HCBV4::Client.from_credentials with proper token persistence --- .../hcb/oauth_connections_controller.rb | 57 ++++++++++++++++ .../hcb/payment_accounts_controller.rb | 66 +++++++++++++++++++ app/models/hcb/oauth_connection.rb | 33 +++++++--- app/models/hcb/payment_account.rb | 9 ++- config/routes.rb | 7 ++ 5 files changed, 160 insertions(+), 12 deletions(-) create mode 100644 app/controllers/hcb/oauth_connections_controller.rb create mode 100644 app/controllers/hcb/payment_accounts_controller.rb diff --git a/app/controllers/hcb/oauth_connections_controller.rb b/app/controllers/hcb/oauth_connections_controller.rb new file mode 100644 index 0000000..86e5f59 --- /dev/null +++ b/app/controllers/hcb/oauth_connections_controller.rb @@ -0,0 +1,57 @@ +class HCB::OauthConnectionsController < ApplicationController + skip_after_action :verify_authorized, only: [:new, :callback, :destroy] + + def new + redirect_to hcb_oauth_authorize_url, allow_other_host: true + end + + def callback + code = params[:code] + if code.blank? + redirect_to root_path, alert: "HCB authorization failed" + return + end + + token = hcb_oauth_client.auth_code.get_token( + code, + redirect_uri: hcb_oauth_callback_url, + ) + + connection = current_user.hcb_oauth_connection || current_user.build_hcb_oauth_connection + connection.update!( + access_token: token.token, + refresh_token: token.refresh_token, + expires_at: token.expires_at ? Time.at(token.expires_at) : nil, + ) + + redirect_to hcb_payment_accounts_path, notice: "HCB account linked! Now create a payment account." + end + + def destroy + current_user.hcb_oauth_connection&.destroy + redirect_to root_path, notice: "HCB account unlinked" + end + + private + + def hcb_oauth_client + @hcb_oauth_client ||= OAuth2::Client.new( + Rails.application.credentials.dig(:hcb, :client_id), + Rails.application.credentials.dig(:hcb, :client_secret), + site: hcb_api_base, + authorize_url: "/oauth/authorize", + token_url: "/oauth/token", + ) + end + + def hcb_api_base + Rails.application.credentials.dig(:hcb, :api_base) || "https://hcb.hackclub.com" + end + + def hcb_oauth_authorize_url + hcb_oauth_client.auth_code.authorize_url( + redirect_uri: hcb_oauth_callback_url, + scope: "read write", + ) + end +end diff --git a/app/controllers/hcb/payment_accounts_controller.rb b/app/controllers/hcb/payment_accounts_controller.rb new file mode 100644 index 0000000..1a17466 --- /dev/null +++ b/app/controllers/hcb/payment_accounts_controller.rb @@ -0,0 +1,66 @@ +class HCB::PaymentAccountsController < ApplicationController + before_action :require_hcb_connection + before_action :set_payment_account, only: [:show, :destroy] + + def index + @payment_accounts = current_user.hcb_payment_accounts + @available_organizations = available_organizations + end + + def new + @organizations = available_organizations + @payment_account = current_user.hcb_payment_accounts.build + end + + def create + org = find_organization(params[:organization_id]) + if org.nil? + redirect_to new_hcb_payment_account_path, alert: "Organization not found" + return + end + + @payment_account = current_user.hcb_payment_accounts.build( + oauth_connection: current_user.hcb_oauth_connection, + organization_id: org.id, + organization_name: org.name, + ) + + if @payment_account.save + redirect_to hcb_payment_accounts_path, notice: "Payment account created for #{org.name}" + else + @organizations = available_organizations + render :new, status: :unprocessable_entity + end + end + + def show + end + + def destroy + @payment_account.destroy + redirect_to hcb_payment_accounts_path, notice: "Payment account removed" + end + + private + + def require_hcb_connection + unless current_user.hcb_connected? + redirect_to new_hcb_oauth_connection_path, alert: "Please link your HCB account first" + end + end + + def set_payment_account + @payment_account = current_user.hcb_payment_accounts.find(params[:id]) + end + + def available_organizations + current_user.hcb_oauth_connection.organizations + rescue => e + Rails.logger.error "Failed to fetch HCB organizations: #{e.message}" + [] + end + + def find_organization(org_id) + available_organizations.find { |o| o.id == org_id } + end +end diff --git a/app/models/hcb/oauth_connection.rb b/app/models/hcb/oauth_connection.rb index 0425a11..5469af7 100644 --- a/app/models/hcb/oauth_connection.rb +++ b/app/models/hcb/oauth_connection.rb @@ -7,23 +7,38 @@ class HCB::OauthConnection < ApplicationRecord validates :user_id, uniqueness: true def client - @client ||= HCBV4::Client.new( + @client ||= HCBV4::Client.from_credentials( client_id: Rails.application.credentials.dig(:hcb, :client_id), client_secret: Rails.application.credentials.dig(:hcb, :client_secret), access_token: access_token, refresh_token: refresh_token, expires_at: expires_at&.to_i, - on_token_refresh: ->(new_access, new_refresh, new_expires) { - update!( - access_token: new_access, - refresh_token: new_refresh, - expires_at: Time.at(new_expires), - ) - }, + base_url: hcb_api_base, ) end def organizations - client.user.organizations + result = client.organizations + persist_refreshed_token! + result + end + + def persist_refreshed_token! + token = client.oauth_token + return unless token.respond_to?(:token) + + if token.token != access_token || token.refresh_token != refresh_token + update!( + access_token: token.token, + refresh_token: token.refresh_token, + expires_at: token.expires_at ? Time.at(token.expires_at) : nil, + ) + end + end + + private + + def hcb_api_base + Rails.application.credentials.dig(:hcb, :api_base) || "https://hcb.hackclub.com" end end diff --git a/app/models/hcb/payment_account.rb b/app/models/hcb/payment_account.rb index 1678488..bfab47b 100644 --- a/app/models/hcb/payment_account.rb +++ b/app/models/hcb/payment_account.rb @@ -14,10 +14,13 @@ class HCB::PaymentAccount < ApplicationRecord end def create_disbursement!(amount_cents:, memo:) - organization.create_transfer( + result = client.create_disbursement( + event_id: organization_id, + to_organization_id: Rails.application.credentials.dig(:hcb, :recipient_org_id), amount_cents: amount_cents, - recipient_organization_id: Rails.application.credentials.dig(:hcb, :recipient_org_id), - memo: memo, + name: memo, ) + oauth_connection.persist_refreshed_token! + result end end diff --git a/config/routes.rb b/config/routes.rb index fd8dc54..92e99e0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -529,6 +529,13 @@ Rails.application.routes.draw do resources :payment_accounts resources :mailer_ids end + + namespace :hcb do + resource :oauth_connection, only: [:new, :destroy] do + get :callback, on: :collection + end + resources :payment_accounts, only: [:index, :new, :create, :show, :destroy] + end resources :source_tags namespace :warehouse do resources :templates