mirror of
https://github.com/System-End/theseus.git
synced 2026-04-19 18:45:15 +00:00
Sign in with Hack Club for back_office users (#185)
* add HCA for back office users * maybe better initializer?
This commit is contained in:
parent
f2acb68255
commit
b1c8b2f91a
12 changed files with 163 additions and 79 deletions
3
Gemfile
3
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"
|
||||
|
|
|
|||
60
Gemfile.lock
60
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@
|
|||
<%= vite_image_tag 'images/login/treasure.png', id: "treasure" %>
|
||||
<%= render 'shared/flash' %>
|
||||
<h1>welcome ashore...</h1>
|
||||
<%= link_to "log in?", slack_auth_path %>
|
||||
<form action="/back_office/auth/hackclub" method="post">
|
||||
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
|
||||
<button type="submit">log in with hack club?</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
8
config/initializers/hack_club_auth.rb
Normal file
8
config/initializers/hack_club_auth.rb
Normal file
|
|
@ -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
|
||||
18
config/initializers/omniauth.rb
Normal file
18
config/initializers/omniauth.rb
Normal file
|
|
@ -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]
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
6
db/migrate/20251211000001_add_hca_id_to_users.rb
Normal file
6
db/migrate/20251211000001_add_hca_id_to_users.rb
Normal file
|
|
@ -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
|
||||
6
db/schema.rb
generated
6
db/schema.rb
generated
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue