diff --git a/Gemfile b/Gemfile index 47534f7..2eeafcf 100644 --- a/Gemfile +++ b/Gemfile @@ -85,6 +85,9 @@ gem "faraday", "~> 2.13" gem "oauth2", "~> 2.0" +gem "omniauth", "~> 2.1" +gem "omniauth_openid_connect", "~> 0.7" + gem "snail", "~> 2.3" gem "easypost", "~> 7.1" diff --git a/Gemfile.lock b/Gemfile.lock index 728a4c8..14f06d6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -94,6 +94,7 @@ GEM kaminari (>= 1.0) sassc-rails (~> 2.1) selectize-rails (~> 0.6) + aes_key_wrap (1.1.0) andand (1.3.3) annotaterb (4.16.0) activerecord (>= 6.0.0) @@ -101,6 +102,7 @@ GEM argon2-kdf (0.3.1) fiddle ast (2.4.3) + attr_required (1.0.2) awesome_print (1.9.2) aws-eventstream (1.4.0) aws-partitions (1.1124.0) @@ -126,6 +128,7 @@ GEM bcrypt_pbkdf (1.1.1-x86_64-darwin) benchmark (0.4.1) bigdecimal (3.2.2) + bindata (2.5.1) bindex (0.8.1) blazer (3.3.0) activerecord (>= 7.1) @@ -175,6 +178,8 @@ GEM dry-cli (1.2.0) easypost (7.1.0) ed25519 (1.4.0) + email_validator (2.2.4) + activemodel erb (5.0.1) erubi (1.13.1) et-orbi (1.2.11) @@ -184,6 +189,8 @@ GEM faraday-net_http (>= 2.0, < 3.5) json logger + faraday-follow_redirects (0.4.0) + faraday (>= 1, < 3) faraday-multipart (1.1.0) multipart-post (~> 2.0) faraday-net_http (3.4.1) @@ -261,6 +268,13 @@ GEM railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (2.12.2) + json-jwt (1.17.0) + activesupport (>= 4.2) + aes_key_wrap + base64 + bindata + faraday (~> 2.0) + faraday-follow_redirects jwt (3.1.1) base64 kamal (2.6.1) @@ -372,6 +386,27 @@ GEM snaky_hash (~> 2.0, >= 2.0.3) version_gem (>= 1.1.8, < 3) observer (0.1.2) + omniauth (2.1.4) + hashie (>= 3.4.6) + logger + rack (>= 2.2.3) + rack-protection + omniauth_openid_connect (0.8.0) + omniauth (>= 1.9, < 3) + openid_connect (~> 2.2) + openid_connect (2.3.1) + activemodel + attr_required (>= 1.0.0) + email_validator + faraday (~> 2.0) + faraday-follow_redirects + json-jwt (>= 1.16) + mail + rack-oauth2 (~> 2.2) + swd (~> 2.0) + tzinfo + validate_url + webfinger (~> 2.0) ostruct (0.6.1) parallel (1.27.0) parser (3.3.8.0) @@ -413,6 +448,17 @@ GEM raabro (1.4.0) racc (1.8.1) rack (3.1.16) + rack-oauth2 (2.3.0) + activesupport + attr_required + faraday (~> 2.0) + faraday-follow_redirects + json-jwt (>= 1.11.0) + rack (>= 2.1.0) + rack-protection (4.2.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) rack-proxy (0.7.7) rack rack-session (2.1.1) @@ -571,6 +617,11 @@ GEM stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.7) + swd (2.0.3) + activesupport (>= 3) + attr_required (>= 0.0.5) + faraday (~> 2.0) + faraday-follow_redirects temple (0.10.3) thor (1.3.2) thruster (0.1.14) @@ -597,6 +648,9 @@ GEM valid_email2 (7.0.13) activemodel (>= 6.0) mail (~> 2.5) + validate_url (1.0.15) + activemodel (>= 3.0.0) + public_suffix version_gem (1.1.8) vite_rails (3.0.19) railties (>= 5.1, < 9) @@ -612,6 +666,10 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) + webfinger (2.1.3) + activesupport + faraday (~> 2.0) + faraday-follow_redirects webrick (1.9.1) websocket (1.2.11) websocket-driver (0.7.7) @@ -676,6 +734,8 @@ DEPENDENCIES nokogiri (~> 1.18) norairrecord (~> 0.4.0) oauth2 (~> 2.0) + omniauth (~> 2.1) + omniauth_openid_connect (~> 0.7) parallel (~> 1.26) pg (~> 1.1) phlex-pdf (~> 0.1.2) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 3211f56..97c6b1b 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,45 +1,8 @@ class SessionsController < ApplicationController - skip_before_action :authenticate_user!, only: [:new, :create] + skip_before_action :authenticate_user!, only: [:omniauth_failure, :hackclub_callback] skip_after_action :verify_authorized - def new - redirect_uri = url_for(action: :create, only_path: false) - Rails.logger.info "Starting Slack OAuth flow with redirect URI: #{redirect_uri}" - redirect_to User.authorize_url(redirect_uri), - host: "https://slack.com", - allow_other_host: true - end - - def create - redirect_uri = url_for(action: :create, only_path: false) - - if params[:error].present? - Rails.logger.error "Slack OAuth error: #{params[:error]}" - uuid = Honeybadger.notify("Slack OAuth error: #{params[:error]}") - redirect_to login_path, alert: "failed to authenticate with Slack! (error: #{uuid})" - return - end - - begin - @user = User.from_slack_token(params[:code], redirect_uri) - rescue => e - Rails.logger.error "Error creating user from Slack data: #{e.message}" - uuid = Honeybadger.notify(e) - redirect_to login_path, alert: "error authenticating! (error: #{uuid})" - return - end - - if @user&.persisted? - session[:user_id] = @user.id - flash[:success] = "welcome aboard!" - redirect_to root_path - else - Rails.logger.error "Failed to create/update user from Slack data" - redirect_to login_path, alert: "are you sure you should be here?" - end - end - def impersonate unless current_user.admin? redirect_to root_path, alert: "you are not authorized to impersonate users. this incident has been reported :-P" @@ -64,4 +27,35 @@ class SessionsController < ApplicationController session[:user_id] = nil redirect_to root_path, notice: "bye, see you next time!" end + + def omniauth_failure + redirect_to login_path, alert: "Authentication failed: #{params[:message]}" + end + + def hackclub_callback + auth = request.env["omniauth.auth"] + + if auth.nil? + redirect_to login_path, alert: "Authentication failed" + return + end + + begin + @user = User.from_hack_club_auth(auth) + rescue => e + Rails.logger.error "Error creating user from Hack Club Auth: #{e.message}" + uuid = Honeybadger.notify(e) + redirect_to login_path, alert: "error authenticating! (error: #{uuid})" + return + end + + if @user&.persisted? + session[:user_id] = @user.id + flash[:success] = "welcome aboard!" + redirect_to root_path + else + Rails.logger.error "Failed to create/update user from Hack Club Auth" + redirect_to login_path, alert: "are you sure you should be here?" + end + end end diff --git a/app/dashboards/user_dashboard.rb b/app/dashboards/user_dashboard.rb index 90586d1..2569c27 100644 --- a/app/dashboards/user_dashboard.rb +++ b/app/dashboards/user_dashboard.rb @@ -14,6 +14,7 @@ class UserDashboard < Administrate::BaseDashboard icon_url: Field::String, is_admin: Field::Boolean, slack_id: Field::String, + hca_id: Field::String, username: Field::String, warehouse_templates: Field::HasMany, home_mid: Field::BelongsTo, @@ -32,7 +33,7 @@ class UserDashboard < Administrate::BaseDashboard is_admin can_warehouse email - slack_id + hca_id ].freeze # SHOW_PAGE_ATTRIBUTES @@ -44,6 +45,7 @@ class UserDashboard < Administrate::BaseDashboard icon_url is_admin slack_id + hca_id username warehouse_templates home_mid @@ -61,6 +63,7 @@ class UserDashboard < Administrate::BaseDashboard icon_url is_admin slack_id + hca_id username home_mid home_return_address diff --git a/app/models/user.rb b/app/models/user.rb index b56de90..23cbca6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -14,6 +14,7 @@ # updated_at :datetime not null # home_mid_id :bigint default(1), not null # home_return_address_id :bigint default(1), not null +# hca_id :string # slack_id :string # # Indexes @@ -45,46 +46,30 @@ class User < ApplicationRecord def remove_admin! = update!(is_admin: false) - def self.authorize_url(redirect_uri) - params = { - client_id: ENV["SLACK_CLIENT_ID"], - redirect_uri: redirect_uri, - state: SecureRandom.hex(24), - user_scope: "users.profile:read,users:read,users:read.email", - } + def self.from_hack_club_auth(auth_hash) + hca_id = auth_hash.dig("uid") + return nil unless hca_id - URI.parse("https://slack.com/oauth/v2/authorize?#{params.to_query}") - end + # Try to find by hca_id first + user = find_by(hca_id: hca_id) - def self.from_slack_token(code, redirect_uri) - # Exchange code for token - response = HTTP.post("https://slack.com/api/oauth.v2.access", form: { - client_id: ENV["SLACK_CLIENT_ID"], - client_secret: ENV["SLACK_CLIENT_SECRET"], - code: code, - redirect_uri: redirect_uri, - }) + # If not found, try to migrate from slack_id + unless user + slack_id = auth_hash.dig("extra", "raw_info", "slack_id") + if slack_id.present? + user = find_by(slack_id: slack_id) + if user + # Migrate user to use hca_id + user.hca_id = hca_id + end + end + end - data = JSON.parse(response.body.to_s) - - return nil unless data["ok"] - - # Get user info - user_response = HTTP.auth("Bearer #{data["authed_user"]["access_token"]}") - .get("https://slack.com/api/users.info?user=#{data["authed_user"]["id"]}") - - user_data = JSON.parse(user_response.body.to_s) - - return nil unless user_data["ok"] - - user = find_by(slack_id: data.dig("authed_user", "id")) return nil unless user - user.email = user_data.dig("user", "profile", "email") - user.username ||= user_data.dig("user", "profile", "username") - user.username ||= user_data.dig("user", "profile", "display_name_normalized") - user.icon_url = user_data.dig("user", "profile", "image_192") || user_data.dig("user", "profile", "image_72") - # Store the OAuth data + user.email = auth_hash.dig("info", "email") + user.username ||= auth_hash.dig("info", "name") + user.save! user end diff --git a/app/views/static_pages/login.html.erb b/app/views/static_pages/login.html.erb index 9bb81a0..ee834a6 100644 --- a/app/views/static_pages/login.html.erb +++ b/app/views/static_pages/login.html.erb @@ -8,6 +8,9 @@ <%= vite_image_tag 'images/login/treasure.png', id: "treasure" %> <%= render 'shared/flash' %>

welcome ashore...

-<%= link_to "log in?", slack_auth_path %> +
+ + +
diff --git a/config/initializers/hack_club_auth.rb b/config/initializers/hack_club_auth.rb new file mode 100644 index 0000000..e1d2c9c --- /dev/null +++ b/config/initializers/hack_club_auth.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +Rails.application.config.hack_club_auth = ActiveSupport::OrderedOptions.new +Rails.application.config.hack_club_auth.client_id = ENV.fetch("HACKCLUB_CLIENT_ID", nil) +Rails.application.config.hack_club_auth.client_secret = ENV.fetch("HACKCLUB_CLIENT_SECRET", nil) +Rails.application.config.hack_club_auth.base_url = ENV.fetch("HACKCLUB_AUTH_URL") do + Rails.env.production? ? "https://auth.hackclub.com" : "https://hca.dinosaurbbq.org" +end diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb new file mode 100644 index 0000000..179fb77 --- /dev/null +++ b/config/initializers/omniauth.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +Rails.application.config.middleware.use OmniAuth::Builder do + provider :openid_connect, + name: :hackclub, + issuer: Rails.application.config.hack_club_auth.base_url, + discovery: true, + client_options: { + identifier: Rails.application.config.hack_club_auth.client_id, + secret: Rails.application.config.hack_club_auth.client_secret, + redirect_uri: ->(env) { "#{env['rack.url_scheme']}://#{env['HTTP_HOST']}/back_office/auth/hackclub/callback" } + }, + scope: %i[openid profile email slack_id] +end + +OmniAuth.config.path_prefix = "/back_office/auth" +OmniAuth.config.request_validation_phase = OmniAuth::AuthenticityTokenProtection.new(key: :_csrf_token) +OmniAuth.config.allowed_request_methods = [:post] diff --git a/config/locales/en.yml b/config/locales/en.yml index a986337..4a55cfb 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -33,6 +33,7 @@ en: user: home_mid: "Home Mailer ID" home_return_address: "Home Return Address" + hca_id: Hack Club Auth ID usps_payment_account: ach: "is ACH?" helpers: diff --git a/config/routes.rb b/config/routes.rb index d365b6b..fd8dc54 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -559,10 +559,9 @@ Rails.application.routes.draw do delete "signout", to: "sessions#destroy", as: :signout get "/login" => "static_pages#login" - end - get "/auth/slack", to: "sessions#new", as: :slack_auth - get "/auth/slack/callback", to: "sessions#create" + get "/auth/hackclub/callback", to: "sessions#hackclub_callback", as: :hackclub_callback + end root "public/static_pages#root", as: :public_root diff --git a/db/migrate/20251211000001_add_hca_id_to_users.rb b/db/migrate/20251211000001_add_hca_id_to_users.rb new file mode 100644 index 0000000..06d5c57 --- /dev/null +++ b/db/migrate/20251211000001_add_hca_id_to_users.rb @@ -0,0 +1,6 @@ +class AddHcaIdToUsers < ActiveRecord::Migration[8.0] + def change + add_column :users, :hca_id, :string + add_index :users, :hca_id, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 8106c2e..6a059c0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_07_29_204357) do +ActiveRecord::Schema[8.0].define(version: 2025_12_11_000001) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "pg_catalog.plpgsql" @@ -373,6 +373,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_29_204357) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.boolean "opted_out_of_map", default: false + t.string "hca_id" + t.index ["hca_id"], name: "index_public_users_on_hca_id", unique: true end create_table "return_addresses", force: :cascade do |t| @@ -411,6 +413,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_29_204357) do t.boolean "can_impersonate_public" t.bigint "home_mid_id", default: 1, null: false t.bigint "home_return_address_id", default: 1, null: false + t.string "hca_id" + t.index ["hca_id"], name: "index_users_on_hca_id", unique: true t.index ["home_mid_id"], name: "index_users_on_home_mid_id" t.index ["home_return_address_id"], name: "index_users_on_home_return_address_id" end