Merge pull request #631 from basecamp/flavorjones/signal-id-integration
Launchpad integration: step 1
This commit is contained in:
+3
-2
@@ -19,7 +19,8 @@ FROM base AS build
|
||||
|
||||
# Install packages needed to build gems
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install --no-install-recommends -y build-essential git libvips pkg-config libyaml-dev
|
||||
apt-get install -y --no-install-recommends -y build-essential git libvips pkg-config libyaml-dev default-libmysqlclient-dev && \
|
||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||
|
||||
# Install application gems
|
||||
COPY Gemfile Gemfile.lock .ruby-version ./
|
||||
@@ -47,7 +48,7 @@ FROM base
|
||||
|
||||
# Install packages needed for deployment
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install --no-install-recommends -y curl libsqlite3-0 libvips build-essential ffmpeg groff libreoffice-writer libreoffice-impress libreoffice-calc mupdf-tools && \
|
||||
apt-get install --no-install-recommends -y curl libsqlite3-0 libvips build-essential ffmpeg groff libreoffice-writer libreoffice-impress libreoffice-calc mupdf-tools default-libmysqlclient-dev && \
|
||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||
|
||||
# Copy built artifacts: gems, application
|
||||
|
||||
@@ -32,6 +32,16 @@ gem "image_processing", "~> 1.14"
|
||||
gem "platform_agent"
|
||||
gem "aws-sdk-s3", require: false
|
||||
|
||||
# 37id and Queenbee integration
|
||||
gem "signal_id", bc: "signal_id", branch: "rails4"
|
||||
gem "mysql2" # needed by signal_id
|
||||
gem "queuety", bc: "queuety", branch: "rails4" # needed by signal_id
|
||||
gem "service_concurrency_prevention", bc: "service_concurrency_prevention" # needed by queuety
|
||||
gem "portfolio", ">= 4.6", bc: "portfolio" # needed by signal_id
|
||||
gem "file_repository", "~> 1.4.5", bc: "file_repository" # needed by portfolio
|
||||
gem "queenbee", bc: "queenbee-plugin"
|
||||
gem "activeresource", require: "active_resource" # needed by queenbee
|
||||
|
||||
# Telemetry, logging, and operations
|
||||
gem "mission_control-jobs"
|
||||
gem "sentry-ruby"
|
||||
@@ -55,4 +65,5 @@ group :test do
|
||||
gem "selenium-webdriver"
|
||||
gem "webmock"
|
||||
gem "vcr"
|
||||
gem "mocha"
|
||||
end
|
||||
|
||||
@@ -15,6 +15,12 @@ GIT
|
||||
railties (>= 8.1.alpha)
|
||||
zeitwerk
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/file_repository
|
||||
revision: 17e90fc35ccb9f0a0dae1ed8623f01386696002d
|
||||
specs:
|
||||
file_repository (1.4.5)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/kamal
|
||||
revision: 344e2d7995e0134aa52f195a984d8d5e24e31d94
|
||||
@@ -32,6 +38,32 @@ GIT
|
||||
thor (~> 1.3)
|
||||
zeitwerk (>= 2.6.18, < 3.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/portfolio
|
||||
revision: 26dd0ccbe4aad5a5c539f90c00e82b6175443636
|
||||
specs:
|
||||
portfolio (5.2.0)
|
||||
activesupport
|
||||
file_repository (>= 1.4.4)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/queenbee-plugin
|
||||
revision: bf887b5c128570d1b19a0140146ae9aa4ae2915f
|
||||
specs:
|
||||
queenbee (3.2.0)
|
||||
activeresource
|
||||
builder
|
||||
rexml
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/queuety
|
||||
revision: bf7e2a552fb674533d3e092f06dbf9b5d1ebc753
|
||||
branch: rails4
|
||||
specs:
|
||||
queuety (4.0.2)
|
||||
activerecord
|
||||
service_concurrency_prevention
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/rails-structured-logging
|
||||
revision: 76960cb5c15fc2b6b5f7542e05d7dcc031cef9e6
|
||||
@@ -40,6 +72,29 @@ GIT
|
||||
json
|
||||
rails (>= 6.0.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/service_concurrency_prevention
|
||||
revision: 320da99431718615ed86f8723e8b9d3259c602a3
|
||||
specs:
|
||||
service_concurrency_prevention (1.1.1)
|
||||
activesupport
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/signal_id
|
||||
revision: 129483ccc1d9452fa7713080fcbf0c012b2f4107
|
||||
branch: rails4
|
||||
specs:
|
||||
signal_id (4.3.2)
|
||||
bcrypt-ruby (>= 3.0.0)
|
||||
google-id-token (>= 1.4.0)
|
||||
mocha
|
||||
multi_json
|
||||
portfolio (>= 4.0)
|
||||
pwned (~> 2.0)
|
||||
queuety (>= 3.0)
|
||||
rails (>= 4.0.2)
|
||||
rails-controller-testing
|
||||
|
||||
GIT
|
||||
remote: https://github.com/crmne/ruby_llm.git
|
||||
revision: a35aa111a0bb36b7f0363b1f1f2a25e3d5378fd3
|
||||
@@ -159,6 +214,14 @@ GEM
|
||||
specs:
|
||||
action_text-trix (2.1.15)
|
||||
railties
|
||||
activemodel-serializers-xml (1.0.3)
|
||||
activemodel (>= 5.0.0.a)
|
||||
activesupport (>= 5.0.0.a)
|
||||
builder (~> 3.1)
|
||||
activeresource (6.1.4)
|
||||
activemodel (>= 6.0)
|
||||
activemodel-serializers-xml (~> 1.0)
|
||||
activesupport (>= 6.0)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
ast (2.4.3)
|
||||
@@ -182,6 +245,8 @@ GEM
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
bcrypt-ruby (3.1.5)
|
||||
bcrypt (>= 3.1.3)
|
||||
bcrypt_pbkdf (1.1.1)
|
||||
benchmark (0.4.1)
|
||||
bigdecimal (3.2.2)
|
||||
@@ -243,6 +308,8 @@ GEM
|
||||
addressable (>= 2.5.0)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
google-id-token (1.4.2)
|
||||
jwt (>= 1)
|
||||
hashdiff (1.2.0)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
@@ -263,6 +330,8 @@ GEM
|
||||
activesupport (>= 5.0.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.12.2)
|
||||
jwt (2.10.1)
|
||||
base64
|
||||
language_server-protocol (3.17.0.5)
|
||||
lint_roller (1.1.0)
|
||||
logger (1.7.0)
|
||||
@@ -292,8 +361,12 @@ GEM
|
||||
railties (>= 7.1)
|
||||
stimulus-rails
|
||||
turbo-rails
|
||||
mocha (2.7.1)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
msgpack (1.8.0)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
mysql2 (0.5.6)
|
||||
net-http (0.6.0)
|
||||
uri
|
||||
net-imap (0.5.8)
|
||||
@@ -343,6 +416,7 @@ GEM
|
||||
public_suffix (6.0.2)
|
||||
puma (6.6.0)
|
||||
nio4r (~> 2.0)
|
||||
pwned (2.4.1)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.16)
|
||||
@@ -355,6 +429,10 @@ GEM
|
||||
rack (>= 1.3)
|
||||
rackup (2.2.1)
|
||||
rack (>= 3)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
actionview (>= 5.0.1.rc1)
|
||||
activesupport (>= 5.0.1.rc1)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
@@ -410,6 +488,7 @@ GEM
|
||||
ruby-vips (2.2.3)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.33.0)
|
||||
@@ -506,6 +585,7 @@ PLATFORMS
|
||||
DEPENDENCIES
|
||||
actiontext-lexical!
|
||||
active_record-tenanted!
|
||||
activeresource
|
||||
aws-sdk-s3
|
||||
bcrypt (~> 3.1.7)
|
||||
bootsnap
|
||||
@@ -513,15 +593,21 @@ DEPENDENCIES
|
||||
bundler-audit
|
||||
capybara
|
||||
debug
|
||||
file_repository (~> 1.4.5)!
|
||||
geared_pagination (~> 1.2)
|
||||
image_processing (~> 1.14)
|
||||
importmap-rails
|
||||
jbuilder
|
||||
kamal!
|
||||
mission_control-jobs
|
||||
mocha
|
||||
mysql2
|
||||
platform_agent
|
||||
portfolio (>= 4.6)!
|
||||
propshaft
|
||||
puma (>= 5.0)
|
||||
queenbee!
|
||||
queuety!
|
||||
rails!
|
||||
rails_structured_logging!
|
||||
redcarpet
|
||||
@@ -532,6 +618,8 @@ DEPENDENCIES
|
||||
selenium-webdriver
|
||||
sentry-rails
|
||||
sentry-ruby
|
||||
service_concurrency_prevention!
|
||||
signal_id!
|
||||
solid_cable (>= 3.0)
|
||||
solid_cache (~> 1.0)
|
||||
solid_queue (~> 1.1)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
include Authentication, CurrentTimezone, SetPlatform, WriterAffinity
|
||||
include Authentication, CurrentRequest, CurrentTimezone, SetPlatform, WriterAffinity
|
||||
|
||||
stale_when_importmap_changes
|
||||
allow_browser versions: :modern
|
||||
|
||||
@@ -2,6 +2,7 @@ module Authentication
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :set_current_account
|
||||
before_action :require_authentication
|
||||
helper_method :authenticated?
|
||||
end
|
||||
@@ -16,6 +17,12 @@ module Authentication
|
||||
skip_before_action :require_authentication, **options
|
||||
before_action :resume_session, **options
|
||||
end
|
||||
|
||||
def require_untenanted_access(**options)
|
||||
skip_before_action :set_current_account, **options
|
||||
skip_before_action :require_authentication, **options
|
||||
before_action :redirect_tenanted_request, **options
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
@@ -40,6 +47,10 @@ module Authentication
|
||||
|
||||
def request_authentication
|
||||
session[:return_to_after_authenticating] = request.url
|
||||
|
||||
# # When we're ready to flip the switch to Launchpad authentication, uncomment this line:
|
||||
# redirect_to Launchpad.login_url(account: Current.account), allow_other_host: true
|
||||
|
||||
redirect_to new_session_path
|
||||
end
|
||||
|
||||
@@ -51,6 +62,9 @@ module Authentication
|
||||
redirect_to root_url if authenticated?
|
||||
end
|
||||
|
||||
def redirect_tenanted_request
|
||||
redirect_to root_url if ApplicationRecord.current_tenant
|
||||
end
|
||||
|
||||
def start_new_session_for(user)
|
||||
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
|
||||
@@ -64,6 +78,10 @@ module Authentication
|
||||
cookies.signed.permanent[:session_token] = { value: session.signed_id, httponly: true, same_site: :lax }
|
||||
end
|
||||
|
||||
def set_current_account
|
||||
Current.account = Account.first
|
||||
end
|
||||
|
||||
def terminate_session
|
||||
Current.session.destroy
|
||||
cookies.delete(:session_token)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
module CurrentRequest
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action do
|
||||
Current.http_method = request.method
|
||||
Current.request_id = request.uuid
|
||||
Current.user_agent = request.user_agent
|
||||
Current.ip_address = request.ip
|
||||
Current.referrer = request.referrer
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
class Sessions::LaunchpadController < ApplicationController
|
||||
require_unauthenticated_access
|
||||
|
||||
before_action :store_sig, only: :show
|
||||
before_action :restore_and_clear_sig, only: :update
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def update
|
||||
if user = Current.account.signal_account.authenticate(sig: @sig).try(:peer)
|
||||
start_new_session_for user
|
||||
redirect_to after_authentication_url
|
||||
else
|
||||
render plain: "Authentication failed. This is probably a bug.", status: :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def store_sig
|
||||
cookies[:_fizzy_launchpad_sig] = params.expect(:sig)
|
||||
end
|
||||
|
||||
def restore_and_clear_sig
|
||||
@sig = cookies.delete :_fizzy_launchpad_sig
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
class Signup::AccountsController < Signup::BaseController
|
||||
before_action :reset_signup_storage
|
||||
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_signup_account_path, alert: "Try again later." }
|
||||
|
||||
def new
|
||||
@signup = Signup.new
|
||||
end
|
||||
|
||||
def create
|
||||
@signup = Signup.new(signup_params)
|
||||
|
||||
if @signup.recognized?
|
||||
store_signup
|
||||
redirect_to Launchpad.authentication_url(purpose: "signup", login_hint: @signup.email_address, redirect_uri: signup_session_url), allow_other_host: true
|
||||
elsif @signup.process
|
||||
redirect_to_account(@signup.account)
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def signup_params
|
||||
params.require(:signup).permit(*Signup::PERMITTED_KEYS)
|
||||
end
|
||||
|
||||
def store_signup
|
||||
@signup.to_h.each { |key, value| signup_storage[key.to_s] = value }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
class Signup::BaseController < ApplicationController
|
||||
require_untenanted_access
|
||||
|
||||
http_basic_authenticate_with(
|
||||
name: Rails.application.credentials.dig(:account_signup_http_basic_auth, :name),
|
||||
password: Rails.application.credentials.dig(:account_signup_http_basic_auth, :password)
|
||||
)
|
||||
|
||||
attr_reader :authenticated_identity
|
||||
|
||||
private
|
||||
def signup_storage
|
||||
session[:signup] ||= {}
|
||||
end
|
||||
|
||||
def reset_signup_storage
|
||||
session.delete(:signup)
|
||||
end
|
||||
|
||||
def authenticated_identity=(identity)
|
||||
@authenticated_identity = identity
|
||||
signup_storage["identity_id"] = identity.try(:id)
|
||||
end
|
||||
|
||||
def set_authenticated_identity
|
||||
if identity_id = signup_storage["identity_id"]
|
||||
@authenticated_identity = SignalId::Identity.find_by(id: identity_id)
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_to_account(account)
|
||||
redirect_to account.signal_account.owner.remote_login_url(proceed_to: root_path),
|
||||
allow_other_host: true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
class Signup::CompletionsController < Signup::BaseController
|
||||
before_action :set_authenticated_identity, only: :create
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def create
|
||||
@signup = Signup.new(signup_params)
|
||||
@signup.signal_identity = authenticated_identity
|
||||
|
||||
if @signup.process
|
||||
reset_signup_storage
|
||||
redirect_to_account(@signup.account)
|
||||
else
|
||||
render plain: "Could not complete signup. This is probably a bug.", status: :unprocessable_entity, layout: false
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def signup_params
|
||||
signup_storage.slice(*Signup::PERMITTED_KEYS.map(&:to_s))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
class Signup::SessionsController < Signup::BaseController
|
||||
def create
|
||||
if self.authenticated_identity = SignalId::Identity.authenticate(params.permit(:sig))
|
||||
redirect_to new_signup_completion_url
|
||||
else
|
||||
render plain: "Authentication failed. This is probably a bug.", status: :unauthorized
|
||||
end
|
||||
end
|
||||
end
|
||||
+10
-1
@@ -1,5 +1,14 @@
|
||||
class Account < ApplicationRecord
|
||||
include Entropic, Joinable
|
||||
include Entropic, Joinable, SignalAccount
|
||||
|
||||
has_many_attached :uploads
|
||||
|
||||
def setup_basic_template
|
||||
user = User.first
|
||||
|
||||
Closure::Reason.create_defaults
|
||||
Collection.create!(name: "Cards", creator: user, all_access: true)
|
||||
workflow = Workflow.create!(name: "Basic")
|
||||
Collection.first.update!(workflow: workflow)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
module Account::SignalAccount
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# TODO: remove the "optional: true" once we've populated the accounts properly
|
||||
belongs_to :signal_account, class_name: "SignalId::Account", primary_key: :queenbee_id, foreign_key: :queenbee_id, optional: true
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def create_with_admin_user(queenbee_id:)
|
||||
new(queenbee_id: queenbee_id).tap do |account|
|
||||
SignalId::Database.on_master do
|
||||
account.name = account.signal_account.name
|
||||
account.save!
|
||||
|
||||
User.create!(
|
||||
name: account.signal_account.owner.name,
|
||||
email_address: account.signal_account.owner.email_address,
|
||||
signal_user_id: account.signal_account.owner.id,
|
||||
role: "admin",
|
||||
password: SecureRandom.hex(36) # TODO: remove password column?
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,7 @@
|
||||
class Current < ActiveSupport::CurrentAttributes
|
||||
attribute :session
|
||||
attribute :http_method, :request_id, :user_agent, :ip_address, :referrer
|
||||
attribute :account
|
||||
|
||||
delegate :user, to: :session, allow_nil: true
|
||||
end
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# lifted from bc3 app/models/launchpad.rb
|
||||
module Launchpad
|
||||
extend self
|
||||
|
||||
delegate :url, to: "SignalId.launchpad"
|
||||
|
||||
def login_url(product: false, account: nil, **params)
|
||||
url product_account_path("/signin", product: product, account: account), params
|
||||
end
|
||||
|
||||
def authentication_url(**params)
|
||||
url "/authenticate", params.merge(product: :fizzy)
|
||||
end
|
||||
|
||||
def product_account_path(path = nil, product: false, account: nil)
|
||||
product_path = "/fizzy" if product || account
|
||||
account_path = "/#{account.signal_account.id}" if account
|
||||
[ product_path, account_path, path ].compact.join
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,159 @@
|
||||
class Signup
|
||||
include ActiveModel::Model
|
||||
include ActiveModel::Attributes
|
||||
include ActiveModel::Validations
|
||||
|
||||
PERMITTED_KEYS = %i[ full_name email_address password company_name ]
|
||||
|
||||
# Input attribute
|
||||
attr_accessor :company_name
|
||||
|
||||
# Input attributes when not using an existing identity
|
||||
attr_accessor :full_name, :email_address, :password
|
||||
|
||||
# Input attribute when using an existing identity, output when creating new identity
|
||||
attr_accessor :signal_identity
|
||||
|
||||
# Output attributes
|
||||
attr_reader :queenbee_account, :signal_account, :account, :user
|
||||
|
||||
validate :validate_new_identity
|
||||
|
||||
def initialize(...)
|
||||
@full_name = nil
|
||||
@email_address = nil
|
||||
@password = nil
|
||||
@company_name = nil
|
||||
@signal_identity = nil
|
||||
@queenbee_account = nil
|
||||
@account = nil
|
||||
@user = nil
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def process(destroy_existing_tenant: false)
|
||||
return false unless valid?
|
||||
|
||||
create_signal_identity
|
||||
create_queenbee_account
|
||||
create_tenant(destroy_existing_tenant:)
|
||||
|
||||
true
|
||||
rescue => error
|
||||
destroy_tenant
|
||||
destroy_queenbee_account
|
||||
destroy_signal_identity
|
||||
|
||||
errors.add(:base, "An error occurred during signup: #{error.message}")
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def recognized?
|
||||
SignalId::Identity.find_by_email_address(email_address).present?
|
||||
end
|
||||
|
||||
def tenant_name
|
||||
@tenant_name ||= signal_account.subdomain
|
||||
end
|
||||
|
||||
def to_h
|
||||
{
|
||||
full_name: full_name,
|
||||
email_address: email_address,
|
||||
company_name: company_name
|
||||
}.compact_blank
|
||||
end
|
||||
|
||||
private
|
||||
def create_signal_identity
|
||||
unless signal_identity.present?
|
||||
SignalId::Database.on_master do
|
||||
@signal_identity = build_new_identity.tap(&:save!)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create_queenbee_account
|
||||
@queenbee_account = Queenbee::Remote::Account.create!(queenbee_account_attributes)
|
||||
SignalId::Database.on_master do
|
||||
@signal_account = SignalId::Account.find_by(queenbee_id: @queenbee_account.id)
|
||||
end
|
||||
end
|
||||
|
||||
def create_tenant(destroy_existing_tenant: false, &block)
|
||||
ApplicationRecord.destroy_tenant(tenant_name) if destroy_existing_tenant
|
||||
ApplicationRecord.create_tenant(tenant_name) do
|
||||
@account = Account.create_with_admin_user(queenbee_id: queenbee_account.id)
|
||||
@account.setup_basic_template
|
||||
@user = User.first
|
||||
end
|
||||
end
|
||||
|
||||
def destroy_tenant
|
||||
if queenbee_account && ApplicationRecord.tenant_exist?(tenant_name)
|
||||
ApplicationRecord.destroy_tenant(tenant_name)
|
||||
@account = nil
|
||||
@user = nil
|
||||
end
|
||||
end
|
||||
|
||||
def destroy_queenbee_account
|
||||
queenbee_account&.cancel
|
||||
@queenbee_account = nil
|
||||
end
|
||||
|
||||
def destroy_signal_identity
|
||||
SignalId::Database.on_master do
|
||||
signal_identity&.destroy
|
||||
end
|
||||
@signal_identity = nil
|
||||
end
|
||||
|
||||
def validate_new_identity
|
||||
unless signal_identity.present?
|
||||
build_new_identity.tap do |identity|
|
||||
unless identity.valid?
|
||||
identity.errors.each { |error| errors.add(error.attribute, error.message) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_new_identity
|
||||
SignalId::Identity.new do |identity|
|
||||
identity.name = full_name || email_address
|
||||
identity.email_address = email_address
|
||||
identity.username = email_address
|
||||
identity.password = password
|
||||
end
|
||||
end
|
||||
|
||||
def queenbee_account_attributes
|
||||
{
|
||||
skip_remote: true, # Fizzy creates its own local account
|
||||
product_name: "fizzy",
|
||||
name: queenbee_account_name,
|
||||
owner_identity_id: signal_identity.id,
|
||||
trial: false,
|
||||
subscription: subscription_attributes,
|
||||
remote_request: request_attributes
|
||||
}
|
||||
end
|
||||
|
||||
def subscription_attributes
|
||||
subscription = FreeV1Subscription
|
||||
{ name: subscription.to_param, price: subscription.price }
|
||||
end
|
||||
|
||||
def request_attributes
|
||||
{ remote_address: Current.ip_address, user_agent: Current.user_agent, referrer: Current.referrer }
|
||||
end
|
||||
|
||||
def queenbee_account_name
|
||||
name = company_name.presence || signal_identity.name
|
||||
name += " (Beta)" if Rails.env.beta?
|
||||
name
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
class Subscription < Queenbee::Subscription
|
||||
SHORT_NAMES = %w[ FreeV1 ]
|
||||
end
|
||||
|
||||
class FreeV1Subscription < Subscription
|
||||
property :proper_name, "Free Subscription"
|
||||
property :price, 0
|
||||
property :frequency, "yearly"
|
||||
end
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
class User < ApplicationRecord
|
||||
include Accessor, ActionText::Attachable, Assignee, Mentionable, Named, Role, Transferable
|
||||
include Accessor, ActionText::Attachable, Assignee, Mentionable, Named, Role, SignalUser, Transferable
|
||||
include Timelined # Depends on Accessor
|
||||
|
||||
has_one_attached :avatar
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module User::SignalUser
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
belongs_to :signal_user, dependent: :destroy, class_name: "SignalId::User", optional: true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
<%= auto_submit_form_with method: :put %>
|
||||
@@ -0,0 +1,58 @@
|
||||
<% @page_title = "Sign up for Fizzy" %>
|
||||
|
||||
<div class="panel shadow center margin-block-double <%= "shake" if flash[:alert] %>" style="--panel-size: 65ch;">
|
||||
<h1 class="txt-xx-large margin-block-end-double">Fizzy</h1>
|
||||
<h2 class="txt-large margin-block-end-double">Create your account</h2>
|
||||
|
||||
<%= form_with model: @signup, url: signup_accounts_path, scope: "signup", class: "flex flex-column gap txt-large", data: { turbo: false } do |form| %>
|
||||
<div class="flex align-center gap">
|
||||
<label class="flex align-center gap input input--actor txt-large">
|
||||
<%= form.text_field :full_name, class: "input", autocomplete: "name", placeholder: "Your name", autofocus: true, required: true %>
|
||||
<%= icon_tag "person", class: "txt-large" %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex align-center gap">
|
||||
<label class="flex align-center gap input input--actor txt-large">
|
||||
<%= form.email_field :email_address, class: "input", autocomplete: "username", placeholder: "Your email address", required: true %>
|
||||
<%= icon_tag "email", class: "txt-large" %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex align-center gap">
|
||||
<label class="flex align-center gap input input--actor txt-large">
|
||||
<%= form.text_field :company_name, class: "input", autocomplete: "organization", placeholder: "Your organization's name", required: true %>
|
||||
<%= icon_tag "building", class: "txt-large" %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex align-center gap">
|
||||
<label class="flex align-center gap input input--actor txt-large">
|
||||
<%= form.password_field :password, class: "input", autocomplete: "new-password", placeholder: "Password (at least 12 characters)", required: true, maxlength: 72, minlength: 12 %>
|
||||
<%= icon_tag "password", class: "txt-large" %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<% if @signup.errors.any? %>
|
||||
<div class="alert alert--error">
|
||||
<ul class="margin-block-none">
|
||||
<% @signup.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<button type="submit" class="btn btn--reversed center">
|
||||
<%= icon_tag "arrow-right" %>
|
||||
<span>Create your account</span>
|
||||
</button>
|
||||
<% end %>
|
||||
|
||||
<footer class="margin-block-start-double txt-center">
|
||||
<p class="txt-small txt-muted">
|
||||
Already have an account?
|
||||
<%= link_to "Sign in", Launchpad.login_url(product: true), class: "decorated" %>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<%= auto_submit_form_with url: signup_completions_path, data: { turbo: false } %>
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
echo "Access with david@37signals.com / secret123456 on http://37signals.fizzy.localhost:3006"
|
||||
echo "Access with david@37signals.com / secret123456 on http://honcho.fizzy.localhost:3006"
|
||||
echo "Access first run on http://first-run.fizzy.localhost:3006"
|
||||
|
||||
if [ -f tmp/solid-queue.txt ]; then
|
||||
# see also config/environments/development.rb
|
||||
export SOLID_QUEUE_IN_PUMA=1
|
||||
fi
|
||||
|
||||
|
||||
@@ -7,8 +7,13 @@ system("gem install bundler --conservative")
|
||||
system("bundle check") || system!("bundle install")
|
||||
|
||||
puts "\n== Preparing database =="
|
||||
system! "bin/rails db:prepare"
|
||||
system! "bin/rails db:reset" if ARGV.include?("--reset")
|
||||
if ARGV.include?("--reset")
|
||||
system! "bin/rails db:drop db:create db:schema:load db:prepare"
|
||||
system! "bin/rails db:fixtures:load" # load signal id fixtures before running fizzy seeds
|
||||
system! "bin/rails db:seed"
|
||||
else
|
||||
system! "bin/rails db:prepare"
|
||||
end
|
||||
|
||||
puts "\n== Removing old logs and tempfiles =="
|
||||
system! "bin/rails log:clear tmp:clear"
|
||||
|
||||
@@ -10,6 +10,7 @@ CI.run do
|
||||
step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
|
||||
|
||||
step "Tests: Rails", "bin/rails test"
|
||||
step "Tests: 37id", "bin/rails 37id:test:units"
|
||||
|
||||
if success?
|
||||
step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
|
||||
|
||||
@@ -1 +1 @@
|
||||
/M5cK3B/Wq1R2f4XsQQ8t7qJGCNMaBU3itDDKNZapeGMjrlE4A9CKOh2RPQsKlrsGjJdgGL4YdfDiT1LiF0pD/KL/t5Fw8Z8GMndzA+hmpvQ+hgufvd8n2ZzK/SIK6RmfC9BabdlvuQ0rr3/EdQJ5yIgk3WtWv73bOWDopqHerEAcaNl8KjCu5n2vokAidqEh0/O30yQQB9PAFMRqEwapLVcDSnaJXr6OQnAAm9tIoWaEaIF8ZJH3Rmn0COs8xZqYFyFv/pP3gVy3Mw+VL3aIkEf/eZ3IQWYNf7fiKPvIdNVsmghmeoQRqCp4E0dpn2XDSEGx5i5oq7ynSDCPky4GahiLtpe3vas+Z+lzD5hAAZmlX0G615b4hWjAViTEQU+CMXhmpj7eVpT9co+1oKmqtbDvHr7uCJIf4d17kC13u/8yjgl2SaCjRWx94wh8su/h6h9Xw0qewITbfM32pFRiH+R5bc=--fxg4Lf+qmYeuvyZp--qVZ+ELV0FRzBb69MUAqOsQ==
|
||||
yD+Wb/DYMXUXf16brcYmNb7OZngx7PuA6gLP8w6ZV7pA/GFweTIXQw67YoXebL6A7gWxhXX4tvpnrz9xPC1p5+5a8q00lOhcybc/480bSfenIXExDpg6mSDbQLRomrpwZsr+IqBWRC8WoXTtpVYRybEPRP88ZXdyE0MjnfOlGcr1fUcF3r9T8biXfNhS9v9UTxck1Q2yzGoNFRvBm3KgBN2QlJocG5AXaDYUSMfLoLmtkicO0KVOD7eYBxe0kDevUK7ZQGxbtATGIsjAJEifhuaPTj/Q2DH134TU4rlPr6n6vm9YEgC/CGVoqPIDgvovVd5IiqZlxGuVDDw3ct0cPH4/AnDLOVHiubi5/pZRCQyeOF+G1OmLVFUVeuZ6alPVrJSxHFxGVM/HT+pLWiReR/IJYacLzb3b2OOtBp/AKxbt25YxFFHuFHABfVEqPRo0qVwbQaxKZEyDyEW0t0ktTkqHywqkbmsx3HPU3skV42rTHf9c1udJhzTXZJPKIWKP96ahgX6eaOhy09rzwD99RnKLJuAuMl80Cw1L+ftLG5hM4tHYmv+JbpWDSInvP1jtA5iWtDRSiqhTxvXw4ItHximvsERI5k6hSPJzrzqnhLUxNVi10KQHh5KGq7EiC2J3HJwioY+fWVivzEJdw2WuVFsovcFyolrT3tyqcMVF5V3dQw4/gOGQ7gXsz0a9sb5wQgXJ9pyZBAmD4Bg7r4wD0f0JiL/5EdKOkAOTV2JDt0OgoSWzU+yuvByuWxPn2quoX0ZSG0huHvl2NK33RoR9aXLcSjtmi5XJGXpjATfKMoA2d8JBIDiJcOPAPwWkpI7AOnmvlV2SmrqPlLTkO0MfdJ528k8CFi2puSRQN+CEPhyhZclDO/J2IP94qmE35xAmWM7yjY6E+EZY9uhlF39uBhAIXylZwTyF31PSCqwpQJthH62pDFWpCgjy1iax482cCVt5n2HLQpmSNaPAvsI7hZAGFJsrDncwfJiK68YXrgmTeg8=--Gd2i+KrusH11oais--SraJ0tcCfEgIdRSJYoCoNw==
|
||||
@@ -1 +1 @@
|
||||
e6HcyVNJvgdJGTCcvW8FDHQ9pHYmTgNW2/71Espdwxd5LQX/Xb9+VkeqpEAL8ceFJo6ro49mMs85G2YngKbbXEh//8+XSS0inMR/kh9wTV3f6YBWnLVBbhrOnYgPcQ8vedsgxLG2Cs4y7ipfeT/dp9bb5m4IxV0OPiUjVFFJuAuGkMJt3pE7D6alqL1ACRxazV9UKV5WKEBfaQYeKpJ++5f86Nls1EwKo5tB1lJed9P5n2yH7meX/QyxjgQd38CIaPwp+F92y5DLKJD5MZRMJGgWf6MT6096LhjEC3vvtS3wwT/eDVcVKTplRkEszj7BZEfOs9Ondb9EVtbXmeoHYSgpaOq9/nEwtho5Uw==--KlHZy1apg3u1XxFs--N2ehRobLHNz+Sepc8p4EIA==
|
||||
hHi9VfqfZI57GYR5r75Rh00L3FYeoUgsdtCgBzJGdPhhGZLGlCgUAcY6tgVhqhgM/u5WxyPAzmoV+bbQKb0wO0U6KUmrDKDv9uRnNSUjXGYsg8gWdTUTfx7WBFOFawV3p1Dsm7zM1oUJbRWgy2TiMFl5kP9CN4Lk78n5a1/Y7mnx/wmzwUyQv+BUFJ0UKCfRmWieaNoLkm5kWwyepWHaSRU5EVda6J9VtuwP3hISIzncXm0jpr9r/tJm8flgUR3o2STIVHVgg+YcWz0j3st5RMv7i3D2OHfNGx84vsf3LNlsiLGB1ZU0IsmchLOSqPmlDK2oiB7UKepTPthGUFzNmcDG4LpUQfNwn7B8P7by0zadVefdLSB3kFCNLX+KCAbcjHPlWufkagwHo+dV+iIVACC+hfWl3L/IouqYS0ln2XCBDz6A8lI8PHeGaJe9UhfA5JEuJLRPui8znoq7LrKo4/oBB7ArKdHz/W/oHJA9+aLHcLOq9TPSExAsGi1wcjdgF0nk5mB9iX+az3//1zM64rRz8EyzNkBg1I6PV6wpzROLubttv1+I5nRVe4BernNfVJPi/hbvrlrcZmouU32oOzd2aZuPL5/H8gBhMOPlN25Rr51XCziaczgdEfOwTO8da6r7pxsJDkrvKi+Ju6efxhZ6GeotWJuEUtQtWhHnzvhcJAXfUtJf6A==--LWtZauLkWq+tD+4L--mkMcqYebO9TDi6RTRNUQvA==
|
||||
@@ -1 +1 @@
|
||||
Qkkt3krcGlOh76//7TmY5rXz31mhL3NNNiO6TCcRhx4lT6uZ8sLjW7X0yp2wZ30y2cJBeIsNMeOhL47JTpu0Z4d0FPtwgH4bzzXAiab0/YrfMjxyBt7/K1DzW/bePUvakJggIvVGGRA1NB1tPkwziQ4dgA34E/UUPAxUofFaoCIFVABmqt4DFg0DKYB8tImDDjZ5xUfSNxrKj3pH5mOIU9jqSK6LtBELFZcNxrJOI0UEgayRNwYAs44ujdM2/2zrAWlUCFZ3NnMU4bBJW02/d6LaxHtB809u8mx3bChEzWMtZB9CvpbRH7u6XfZJR1YGFztcNguzRbNX5x9pKZImAuAhVFKR7Fbi12Hg3w7U3A/YovwmyhHK6ksfOzYlYoDUTBnGYVwiqR16naaHCMekomK+uo0qtOn83RQkLuuqFJshPXn/lW8Rggt4dq/0THEl7Qj+dumadlWDVEgxhipI031RSBF1toNWw/zwaC31Ord1YXD8ZC09sPoj8Zd4hD2o2a0IUuIMXA8w5/2+AmdU8wF66opdKGdsZDRjM2Bul8aAbJ7/iV7xTuvlE4LmzXGArMvivIZ0GwrN3YAsDta2Y3ufibq7WZzSCOrpbPKJ9CEGg4a2AN8UGaGZ5ZEAYg8lhDgsZMjHRkGNuxk8QkDiVFDyNoI7H9ZQqbw+L1AJRTfDIcNB/9h5n+//Nk4ylLF1lnI7y/IQA/AuSQ+UN+okt3RhLugwGvWrs49MExJStLHpGveSoQ==--MQ2JycR3X6LIiKSY--45C7bleKX9FMGv4fc+G1yw==
|
||||
cPaSSbJ2XWIjNXDRi25iIH41V4zR1fKVTe3OV1XwZKsDE3950DL9Sk0RI/1bxYY+agT3x88vrsqm8xyqLwqcMeMg7J4el52WI4hORe5XLGJTcvs8CTgguut1s7c5Jb5ihOSPX9ZYEKnONxLU2BFe951WZgO+7Us2IBFbCQiqo8PXEBEONePuE9Aw2z1fUFW7NvxhptN9ws7DptP2O/wpHP8+KJmVuAJl+/qJHJoBh770dwQU3yCFz9mERowkARK4Z16Q+HelkrkvW9UziV53tMIExDRvfMnEKJz4wrU/AqVSiQIMQA6eVfTMQLxbVhBhLAlcV0Qic7yEbXLcAXwLX6Kga6aMOqyJvxh+JUnMpQavtHKikVncN8EI1lGxd/+mddS+khHZmCqkRvWl+LiaOe2WOkNENYXlGYANeRSTDy2BqMeT1ymAj3YxfWKYdR34Ukmm2iZKQ1d6fF+Dg/IT8/W/rIZPajQjMTO05WqeoE8kZ4lhgI7AxZHg9apHGsfkq6iBaf4kZUBFiLgOv0CR4Xz3zWVUHEyPx5afkkxs1u9NCspOaYWT4gA1tekFmaH7cMyhaQ/SEjOEs5AWyh47PeMBvwUumJKIEeil6p6K9109nSFG+BPszfhshY8G3aonuIbiWoJQAiGGnbvw4oZeAPjl8XNBYTWRspl2T9ep3krPg9L4eTrAyXdT0dG7GYXSa5NkrV86HugU5rDfx87N4QJVu6qr7Myzw6kEWlLRXHR6HPg+H7qZenNwuOfgnD3YoFOEWQfLiz9U3MqDRJIq5j+i2OfL4pv0VVpc51z42mWM1hvazGNiiTZvK0DohBja6D9h5Gl6ZP/eOGB8O6ZxIwn6mp23BIvxA5jBRGslCj/Op7msnkSAP0vtuI0hnCtfsOx0RfAPvj1/ETjVkRFhM7heWTgfqGQbjSVGvd/Dj6j3vbqGz3RASVL48juoWx/pezUMnWGZqRTqx5tD6FrZQy1yf/2Uoc8yMJQU14raqiShEqNveJswMv1D9kBnT3551DtpAmrMT+iuCX5aZHwVnY/sI8J8HdMAqFfpipMbCbccI/PgTc+U8uOqc+BBKYy8u04wHLnGu5wREewU5xRIkxcwGm4M4LkTLiVEJFUQuFEhylmxkCIDYG+WSBZ5xy/0dRUeZs26Uuvvs2//bXqn75Ia86Fpv54AzfdDqC9s0B1r5/ZqXtFjjL3bQyhv+SP5TMurAZcI0rLYay+oFcLZAv31zoo0dFcufiCKYTjbYEIlO+HW2aeVeOhUnCalqqslt4ql5A==--tVMd1KheRgH8F0gZ--x8Wzmk8JsMuHPG3JMOODLA==
|
||||
@@ -1 +1 @@
|
||||
Tc514fYVlm1c2c81KUFmuAGZ9rSZT7XfeGUEdTEvJNDD6JE9IghKbAAGgc6AalIt2/VxzSV6AGvNagb0/qlJrMOFeSJLqzufKt1YoLRUZtSUZDtzmI/2WSzju0Tyu4za6rnLEu+WvDc0NM8KZEv/TwwUbSOwoEVDQmava/iuWyvFr5sOH1wNr2kwYbErwDpThKcYqacI4CdvgpgXc//3T0Z5vsVCs6d8723DymM0KkSK9so8WrcCesk2fmhM1K2hQV7QdckaVuOL0JEj1AI7KzdJm7REoeLwRwaBeVDkqDNo2fKxwZ2XmU2ViJOozPsowx+324wQQZjbsf/0ohtDkgjbfMmA2tjkuNJdJFwIIc5VyvILhVs5WQIJHSXmabhUKxzKRCRZNBSaadLGCb7/AGfULKvh2vP1BWqm80EkDmhHm42ucJW1vA377y9UYWVcdvqWtp5m2F7HlXqi9Uw/+kFV2X6Jw1c07FsCu35TrUb5u8G7mu/866GBgf5VlfDVs1BA3aru05CtcsXi2qKMw0II7fZps/4XpYCB+VehXAyLi2ABVeTnqSfF7HwasXvfD3Jgh7zJLD0hudNDpu6xkmoyeksVBn034dmQf0TwAIKmNjEKMDxgAG50T2JhnEvHZEnqzGl6O8if8DKM5rPgt3XEU3zN/U3yA8T0eTyFdGlWbYK7Ergu4TcGo3WPk6bJhTGOHQ9puD89t6JGHbKDYW77w44SHv+C42flmNkzcndXlpvZvnEafDYeW6ofRC4eeTRX4+WKL4CYhlel78+twD6Mh0MV42MYM2rmtl66NHN8fWxyfoyjrNcXveKVAsGMatB0+dOglq6GmOiXRAt/I6D+7QYZLyaUhYNmmiuZXF4=--H6KWPnHc9qUQE5AI--PN98cg9t9ZhxMUEax/Mfgg==
|
||||
UV9AzQwwjGoWFh78GyKkNewL4lmsL80XT+6ca0JIxZ632OMKbYzHbPtJAD6RK1gaEB4GRkiBE2mLEdk8nYI47p8K9NVqZuI/ykgl23Sr2JzBpnHfjtUYlPQC7nYyeAR3gYtASD5+jSXvP7z/9BquIVdTvih3hWPPujDK5OROXagDtlGTZ4SgKZwihCTZczTwZRLxx5UguwbtuowrtcuPTg+r/H+zdJyRUU9kF7vYqV5VrP0vj0mXZUf5hWNBpKvhDtqgKUJNU87ds7xTLwMuQT0MCASoJXS0JSpIKn653baXczQue6JyuFcOlqDPDmBWs2GLh6pq3XnNolmWoyNehD9WClmJ5cu0l1dPmYZ0WnOfPgJCPtuJI4onIUj2nLC5m0tHpQPNs9WjazQeCeiw7TJBESK3b3lB4/Q7STshfJd74IIJ0crzfm0a6MwxvT184etKpNRQkgJpwK6WRNPzzzHdlTmbkEher4ilcs3QrmyLPYaLcuG+0YyDcjgJXRvLumkZilONbuBHluMc64u4GAU8QyW1dp31DrCw/HTOTtPEsRJSKdTIiFFCDpYt0Uq16Qbp9Hsg1ruPqFPELXDqRSl+N9ixDdXjF8WyQc6Gx4i8Fsur0WBLGBEtCEkCCQvwsH+TVH47S68CSiUui0c2zX1sM2SEcOOL7Cn0TUDWg6d8MgHQ5977aGLGFUkeuldv8KfQSra5uHKESSxOwn/eJJQMERSiVHSEj1EK3v4R+shXnviPsFTr3uERjdn2s1CcSk3qJ7J5WFpmgTL3/qKRWDroI96QO24CSr8JWJ3MzrwkLLpiEf6qLyfswiRjJ79sxk1pTdx7zedb8MuG/596J/xFI7GdbOI6qr2NvVUHH6r6PqXltMBoT/JU4pO7QOXfuok8zYGOj2gMzktCOdvEoydh+8QrsKN6W3mpg8oweQoq/2tgRm/YTeJMz+MQMR+J+aid2lSu8/wiHtsv8WdxJ7qxI7G3DslzbTEz0e3FAP+CEFOVgwN+nitWZpSyst6WiupzU5Ou9NxYiWbmhy5nhDJFC+MiLs8bS2kWfq3z1/Wsuyp8hBn/T0+MYtQ/cRnAxTE0sUZMB5xpffuGPlx3lgrmxV7wlPAzGooCGkGVUQQ29ljYMoGWqW34EAniIYHLUnBfdS+bNbui5rxVEAFyD4r57eqEaYYOKCFwkhNfYhHUeV+noDQ8uvCzppvRHGKJrkUCS0/A6z4KP8iW8wsETMBX/ShvFf6m44KNGR4IPzfn+r7uO9Pbukit3RJ8RsHZcC3RmH7/gtO3vW8G1BBrT5nI6BdRp9a9LdTx2qn32cKQD7Wc7S1KyFYn5reWSewlxNHHmgy4R/pEXTtdZJ5tuzweeYK/jr78CTrgEEN/btNawNh6SdTwRls5lu33LAQ=--3mgI5AzJFOeaM6Y1--KgIowEbtW/iHXiDrWGk2ug==
|
||||
@@ -1 +1 @@
|
||||
1QS4QGl05L2YvkEusirttYv2gIfRbNLEWTK7pOXG/HxqyWbWj9o5/GbTFeLI7K0sbcEjz+gcPcVKB+FGnkJECExynSvIN02bg5gP0QegTC3WA2vWInlr+gpoWCNgsazOgHKMxDBnQXc2myqhwDLKRsDHCQTW/5lIiKGBtBOLaml8KrJg84SeYCh54tJUjAp5yRkrhJQBHVgSSUfXRaFcrmdbPls5FDgB+rErACpp4BgYdXZoDds2RITDgQ==--kpoxQbOswL3cInhK--r+dB3hJHvjfUH1EBNzBLSg==
|
||||
0EWG0dpq/qELmTCqAgzEL2BZH7JCNFe6DDIynyKE06qiBCYhHs5+fMjmpzN0uwz43tqAAKJdhO/hZY4jXmTpxu+LSjwrUKAlxsWvSGu4vHEbJ5Fe+BzhnS84OFse6ma9akUAHaed3gt31wtrt2Y4AbSPedNvriqfYln7cvpgWbvjTgtEbHrcNMfjCBnaXXdO3n1zILunILgLiV7oBW0qurIrl2xOc4mbj4AqVAIElyh0md9fFtuBfsaRj2U9x/+7McbgTFODhHfeqoErEw7fc1RBFrNi2YNuHBSSqegQ6WtWbwq1Yg2Juv/Lw3g4IIdWadJsW3TdTMMiwmBMzOikx3lNX1Z399glvGNFVnJfBubKf6BxvFfAQ85PBzOZPl+/PTs=--GY44JMs5ZO1/eL6G--EhxEWuitiex6eHROpPQamw==
|
||||
@@ -0,0 +1,6 @@
|
||||
Queenbee.host_app = Fizzy
|
||||
|
||||
Rails.application.config.to_prepare do
|
||||
Queenbee::Subscription.short_names = Subscription::SHORT_NAMES
|
||||
Queenbee::ApiToken.token = Rails.application.credentials.dig(:queenbee_api_token)
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
require "signal_id"
|
||||
|
||||
ENV["SIGNAL_ID_SECRET"] = Rails.application.credentials.signal_id_secret
|
||||
|
||||
Rails.application.config.to_prepare do
|
||||
SignalId.product = "fizzy"
|
||||
|
||||
SignalId::Database.load_configuration SignalId::Database.default_configuration
|
||||
SignalId::Database.enable_rw_splitting!
|
||||
|
||||
silence_warnings do
|
||||
SignalId::Account::Peer = Account
|
||||
SignalId::User::Peer = User
|
||||
end
|
||||
end
|
||||
|
||||
Rails.application.config.after_initialize do
|
||||
ActiveRecord.yaml_column_permitted_classes << SignalId::PersonName
|
||||
end
|
||||
@@ -2,10 +2,17 @@ Rails.application.config.after_initialize do
|
||||
# in production and staging, we're using a two-level subdomain like "tenant.fizzy.37signals.com".
|
||||
# in development and beta it's only a single-level subdomain.
|
||||
Rails.application.config.active_record_tenanted.tenant_resolver = ->(request) do
|
||||
next nil if request.path == "/up"
|
||||
|
||||
tld_length = [ "37signals.com", "37signals-staging.com" ].include?(request.domain) ? 2 : 1
|
||||
request.subdomain(tld_length).presence || "return-404" # this is a quick fix for now
|
||||
|
||||
if request.path == "/up"
|
||||
nil
|
||||
elsif subdomain = request.subdomain(tld_length).presence
|
||||
subdomain
|
||||
elsif request.path =~ %r{^/(queenbee|signup)\b}
|
||||
nil
|
||||
else
|
||||
"return-404" # this is a quick fix for now
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -85,6 +85,14 @@ Rails.application.routes.draw do
|
||||
scope module: "sessions" do
|
||||
resources :transfers, only: %i[ show update ]
|
||||
end
|
||||
resource :launchpad, only: %i[ show update ], controller: "sessions/launchpad"
|
||||
end
|
||||
|
||||
namespace :signup do
|
||||
get "/" => "accounts#new"
|
||||
resources :accounts, only: %i[ new create ]
|
||||
get "/session" => "sessions#create" # redirect from Launchpad after mid-signup authentication
|
||||
resources :completions, only: %i[ new create ]
|
||||
end
|
||||
|
||||
resources :users do
|
||||
@@ -150,6 +158,8 @@ Rails.application.routes.draw do
|
||||
|
||||
root "events#index"
|
||||
|
||||
Queenbee.routes(self)
|
||||
|
||||
namespace :admin do
|
||||
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||
end
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class AddQueenbeeIdToAccounts < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
add_column :accounts, :queenbee_id, :integer # I solemnly swear I will add a "NOT NULL" constraint once it's populated
|
||||
add_index :accounts, :queenbee_id, unique: true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
class AddSignalUserIdToUsers < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
add_column :users, :signal_user_id, :integer
|
||||
add_index :users, :signal_user_id, unique: true
|
||||
end
|
||||
end
|
||||
Generated
+5
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2025_06_09_102553) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2025_06_12_163028) do
|
||||
create_table "accesses", force: :cascade do |t|
|
||||
t.integer "collection_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
@@ -26,7 +26,9 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_09_102553) do
|
||||
t.datetime "created_at", null: false
|
||||
t.string "join_code"
|
||||
t.string "name", null: false
|
||||
t.integer "queenbee_id"
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["queenbee_id"], name: "index_accounts_on_queenbee_id", unique: true
|
||||
end
|
||||
|
||||
create_table "action_text_rich_texts", force: :cascade do |t|
|
||||
@@ -334,9 +336,11 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_09_102553) do
|
||||
t.string "name", null: false
|
||||
t.string "password_digest"
|
||||
t.string "role", default: "member", null: false
|
||||
t.integer "signal_user_id"
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["email_address"], name: "index_users_on_email_address", unique: true
|
||||
t.index ["role"], name: "index_users_on_role"
|
||||
t.index ["signal_user_id"], name: "index_users_on_signal_user_id", unique: true
|
||||
end
|
||||
|
||||
create_table "watches", force: :cascade do |t|
|
||||
|
||||
+54
-2
@@ -113,6 +113,16 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: queenbee_id
|
||||
cast_type: *1
|
||||
sql_type_metadata: *2
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *9
|
||||
action_text_rich_texts:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
@@ -983,6 +993,16 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: signal_user_id
|
||||
cast_type: *1
|
||||
sql_type_metadata: *2
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *9
|
||||
watches:
|
||||
- *22
|
||||
@@ -1163,7 +1183,23 @@ indexes:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
accounts: []
|
||||
accounts:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: accounts
|
||||
name: index_accounts_on_queenbee_id
|
||||
unique: true
|
||||
columns:
|
||||
- queenbee_id
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
action_text_rich_texts:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: action_text_rich_texts
|
||||
@@ -2250,6 +2286,22 @@ indexes:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: users
|
||||
name: index_users_on_signal_user_id
|
||||
unique: true
|
||||
columns:
|
||||
- signal_user_id
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
watches:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: watches
|
||||
@@ -2301,4 +2353,4 @@ indexes:
|
||||
comment:
|
||||
valid: true
|
||||
workflows: []
|
||||
version: 20250609102553
|
||||
version: 20250612163028
|
||||
|
||||
+38
-11
@@ -7,24 +7,52 @@ def seed_account(name)
|
||||
puts " #{elapsed.round(2)} sec"
|
||||
end
|
||||
|
||||
def create_tenant(name)
|
||||
ApplicationRecord.destroy_tenant name
|
||||
ApplicationRecord.create_tenant name
|
||||
ApplicationRecord.current_tenant = name
|
||||
def create_tenant(signal_account_name)
|
||||
signal_account = SignalId::Account.find_by_product_and_name!("fizzy", signal_account_name)
|
||||
tenant_name = signal_account.subdomain
|
||||
|
||||
ApplicationRecord.destroy_tenant tenant_name
|
||||
ApplicationRecord.create_tenant(tenant_name) do
|
||||
account = Account.create_with_admin_user(queenbee_id: signal_account.queenbee_id)
|
||||
account.setup_basic_template
|
||||
end
|
||||
|
||||
ApplicationRecord.current_tenant = tenant_name
|
||||
end
|
||||
|
||||
def create_first_run(name, email_address, password: "secret123456")
|
||||
FirstRun.create!(name:, email_address:, password:)
|
||||
def find_or_create_user(full_name, email_address)
|
||||
SignalId::Database.on_master do
|
||||
unless signal_identity = SignalId::Identity.find_by_email_address(email_address)
|
||||
signal_identity = SignalId::Identity.create!(
|
||||
name: full_name,
|
||||
email_address: email_address,
|
||||
username: email_address,
|
||||
password: "secret123456"
|
||||
)
|
||||
end
|
||||
|
||||
signal_account = Account.first.signal_account
|
||||
signal_user = SignalId::User.find_or_create_by!(identity: signal_identity, account: signal_account)
|
||||
|
||||
if user = User.find_by(signal_user_id: signal_user.id)
|
||||
user.password = "secret123456"
|
||||
user.save!
|
||||
user
|
||||
else
|
||||
User.create!(
|
||||
signal_user_id: signal_user.id,
|
||||
name: signal_identity.name,
|
||||
email_address: signal_identity.email_address,
|
||||
password: "secret123456"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def login_as(user)
|
||||
Current.session = user.sessions.create
|
||||
end
|
||||
|
||||
def create_user(name, email_address, password: "secret123456")
|
||||
User.create!(name:, email_address:, password:)
|
||||
end
|
||||
|
||||
def create_collection(name, creator: Current.user, all_access: true, access_to: [])
|
||||
Collection.create!(name:, creator:, all_access:).tap { it.accesses.grant_to(access_to) }
|
||||
end
|
||||
@@ -36,4 +64,3 @@ end
|
||||
# Seed accounts
|
||||
seed_account "37signals"
|
||||
seed_account "honcho"
|
||||
seed_account "first-run"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
create_tenant "37signals"
|
||||
|
||||
david = create_first_run "David Heinemeier Hansson", "david@37signals.com"
|
||||
login_as david
|
||||
david = find_or_create_user "David Heinemeier Hansson", "david@37signals.com"
|
||||
jz = find_or_create_user "Jason Zimdars", "jz@37signals.com"
|
||||
kevin = find_or_create_user "Kevin Mcconnell", "kevin@37signals.com"
|
||||
|
||||
jz = create_user "Jason Zimdars", "jz@37signals.com"
|
||||
kevin = create_user "Kevin Mcconnell", "kevin@37signals.com"
|
||||
login_as david
|
||||
|
||||
create_collection("Fizzy", access_to: [ jz, kevin ]).tap do |fizzy|
|
||||
create_card("Prepare sign-up page", description: "We need to do this before the launch.", collection: fizzy)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
create_tenant "first-run"
|
||||
+8
-8
@@ -1,15 +1,15 @@
|
||||
create_tenant "honcho"
|
||||
create_tenant "Honcho"
|
||||
|
||||
david = find_or_create_user "David Heinemeier Hansson", "david@37signals.com"
|
||||
jason = find_or_create_user "Jason Zimdars", "jz@37signals.com"
|
||||
kevin = find_or_create_user "Kevin McConnell", "kevin@37signals.com"
|
||||
jorge = find_or_create_user "Jorge Manrubia", "jorge@37signals.com"
|
||||
mike = find_or_create_user "Mike Dalessio", "mike@37signals.com"
|
||||
|
||||
david = create_first_run "David Heinemeier Hansson", "david@37signals.com"
|
||||
login_as david
|
||||
|
||||
jason = create_user "Jason Zimdars", "jz@37signals.com"
|
||||
kevin = create_user "Kevin Mcconnell", "kevin@37signals.com"
|
||||
sarah = create_user "Sarah Johnson", "sarah@37signals.com"
|
||||
mike = create_user "Mike Peterson", "mike@37signals.com"
|
||||
|
||||
# Array of authors for random assignment
|
||||
authors = [ david, jason, kevin, sarah, mike ]
|
||||
authors = [ david, jason, kevin, jorge, mike ]
|
||||
|
||||
# Card titles for reuse across collections
|
||||
card_titles = [
|
||||
|
||||
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require_relative "../config/environment"
|
||||
|
||||
ActiveRecord::Base.logger = Logger.new(File::NULL)
|
||||
|
||||
class BootstrapSignalId
|
||||
def initialize(dry_run: false)
|
||||
@dry_run = dry_run
|
||||
end
|
||||
|
||||
def run
|
||||
SignalId::Database.on_master do
|
||||
ApplicationRecord.with_each_tenant do |tenant|
|
||||
puts "\n# tenant: #{tenant}"
|
||||
|
||||
next unless check_account_preconditions
|
||||
create_signal_id_account if Account.first.queenbee_id.nil?
|
||||
|
||||
create_signal_id_users
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def check_account_preconditions
|
||||
unless Account.count == 1
|
||||
puts "There are #{Account.count} accounts, but exactly one is expected."
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def create_signal_id_account
|
||||
owner = SignalId::Identity.find_by_email_address!("kevin@37signals.com")
|
||||
print_identity("New owner is:", owner)
|
||||
|
||||
unless @dry_run
|
||||
qbattr = queenbee_account_attributes(owner)
|
||||
queenbee_account = Queenbee::Remote::Account.create!(qbattr)
|
||||
|
||||
signal_id_account = SignalId::Account.find_by!(queenbee_id: queenbee_account.id)
|
||||
signal_id_account.update_column :subdomain, ApplicationRecord.current_tenant
|
||||
|
||||
account = Account.first
|
||||
account.queenbee_id = queenbee_account.id
|
||||
account.name = ApplicationRecord.current_tenant
|
||||
account.save!
|
||||
end
|
||||
end
|
||||
|
||||
def create_signal_id_users
|
||||
signal_account = Account.first.signal_account
|
||||
|
||||
User.find_each do |user|
|
||||
if !user.system? && user.signal_user_id.nil?
|
||||
signal_identities = SignalId::Identity.where(email_address: user.email_address)
|
||||
if signal_identities.length > 1
|
||||
puts "Multiple identities found for #{user.email_address}:"
|
||||
signal_identities.each { |identity| print_identity(" - ", identity) }
|
||||
signal_identity = signal_identities.first
|
||||
elsif signal_identities.length == 1
|
||||
signal_identity = signal_identities.first
|
||||
print_identity("Identity for #{user.email_address}:", signal_identity)
|
||||
else
|
||||
puts "No identity found for #{user.name} (#{user.email_address})"
|
||||
signal_identity = nil
|
||||
end
|
||||
|
||||
if signal_identity
|
||||
unless @dry_run
|
||||
signal_user = SignalId::User.find_or_create_by!(identity: signal_identity, account: signal_account)
|
||||
|
||||
user.signal_user_id = signal_user.id
|
||||
user.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def print_identity(message = "Identity:", identity)
|
||||
pad = " " * message.length
|
||||
puts "#{message} #{identity.name} (#{identity.email_address})"
|
||||
puts "#{pad } ID: #{identity.id}"
|
||||
puts "#{pad } Username: #{identity.username}"
|
||||
end
|
||||
|
||||
def queenbee_account_attributes(signal_identity)
|
||||
{
|
||||
skip_remote: true, # Fizzy creates its own local account
|
||||
product_name: "fizzy",
|
||||
name: account_name,
|
||||
owner_identity_id: signal_identity.id,
|
||||
trial: false,
|
||||
subscription: subscription_attributes,
|
||||
remote_request: request_attributes
|
||||
}
|
||||
end
|
||||
|
||||
def subscription_attributes
|
||||
subscription = FreeV1Subscription
|
||||
{ name: subscription.to_param, price: subscription.price }
|
||||
end
|
||||
|
||||
def request_attributes
|
||||
{ user_agent: "script/bootstrap-signal-id.rb" }
|
||||
end
|
||||
|
||||
def account_name
|
||||
name = ApplicationRecord.current_tenant
|
||||
name += " (Beta)" if Rails.env.beta?
|
||||
name
|
||||
end
|
||||
end
|
||||
|
||||
BootstrapSignalId.new(dry_run: false).run
|
||||
@@ -0,0 +1,30 @@
|
||||
require "test_helper"
|
||||
|
||||
class Sessions::LaunchpadControllerTest < ActionDispatch::IntegrationTest
|
||||
test "show renders when not signed in" do
|
||||
get session_launchpad_path(params: { sig: "test-sig" })
|
||||
|
||||
assert_response :success
|
||||
|
||||
assert_equal cookies[:_fizzy_launchpad_sig], "test-sig"
|
||||
end
|
||||
|
||||
test "create establishes a session when the sig is valid" do
|
||||
user = users(:david)
|
||||
cookies[:_fizzy_launchpad_sig] = user.signal_user.perishable_signature
|
||||
|
||||
put session_launchpad_path
|
||||
|
||||
assert_redirected_to root_url
|
||||
assert parsed_cookies.signed[:session_token]
|
||||
end
|
||||
|
||||
test "returns 401 when the sig is invalid" do
|
||||
user = users(:david)
|
||||
cookies[:sig] = "not-valid"
|
||||
|
||||
put session_launchpad_path
|
||||
|
||||
assert_response :unauthorized
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,108 @@
|
||||
require "test_helper"
|
||||
|
||||
class Signup::AccountsControllerTest < ActionDispatch::IntegrationTest
|
||||
test "new under a tenanted domain redirects to the root" do
|
||||
get new_signup_account_url
|
||||
|
||||
assert_redirected_to root_url
|
||||
end
|
||||
|
||||
test "new under an untenanted domain is OK" do
|
||||
integration_session.host = "example.com" # no subdomain
|
||||
|
||||
get new_signup_account_url, headers: auth_headers
|
||||
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "create with invalid params" do
|
||||
integration_session.host = "example.com" # no subdomain
|
||||
|
||||
post signup_accounts_url,
|
||||
headers: auth_headers,
|
||||
params: { signup: { full_name: "Jim", email_address: "jim@example.com", password: "", company_name: "" } }
|
||||
|
||||
assert_response :unprocessable_entity
|
||||
assert_select "div.alert--error", text: /you need to choose a password/i
|
||||
end
|
||||
|
||||
test "create for a new " do
|
||||
integration_session.host = "example.com" # no subdomain
|
||||
|
||||
assert_difference -> { SignalId::Identity.count }, +1 do
|
||||
assert_difference -> { SignalId::Account.count }, +1 do
|
||||
post signup_accounts_url, headers: auth_headers,
|
||||
params: {
|
||||
signup: {
|
||||
full_name: "Jim",
|
||||
email_address: "jim@example.com",
|
||||
password: SecureRandom.hex(12),
|
||||
company_name: "signup-accounts-controller-test-1"
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
signal_account = SignalId::Account.last
|
||||
assert_redirected_to(/#{signal_account.login_url}/)
|
||||
end
|
||||
|
||||
test "create for an existing identity" do
|
||||
integration_session.host = "example.com" # no subdomain
|
||||
|
||||
identity = signal_identities(:david)
|
||||
|
||||
post signup_accounts_url, headers: auth_headers,
|
||||
params: { signup: { email_address: identity.email_address, company_name: "signup-accounts-controller-test-2" } }
|
||||
|
||||
assert_authentication_requested_for identity
|
||||
|
||||
assert_no_difference -> { SignalId::Identity.count } do
|
||||
assert_difference -> { SignalId::Account.count } do
|
||||
authenticate_via_launchpad_as(identity)
|
||||
assert_redirected_to_account
|
||||
assert_nil session[:signup]
|
||||
end
|
||||
end
|
||||
|
||||
signal_account = SignalId::Account.last
|
||||
ApplicationRecord.with_tenant(signal_account.subdomain) do
|
||||
assert_equal Account.last, signal_account.peer
|
||||
end
|
||||
end
|
||||
|
||||
test "actions require HTTP basic authentication while we're in internal-only mode" do
|
||||
integration_session.host = "example.com" # no subdomain
|
||||
|
||||
get new_signup_account_url
|
||||
|
||||
assert_response :unauthorized
|
||||
end
|
||||
|
||||
private
|
||||
def auth_headers
|
||||
{
|
||||
"HTTP_AUTHORIZATION" => ActionController::HttpAuthentication::Basic.encode_credentials(
|
||||
Rails.application.credentials.dig(:account_signup_http_basic_auth, :name),
|
||||
Rails.application.credentials.dig(:account_signup_http_basic_auth, :password)
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
def assert_authentication_requested_for(identity)
|
||||
assert_response :redirect
|
||||
assert_match(/#{Regexp.escape(Launchpad.url("/authenticate", login_hint: identity.email_address))}.*&purpose=signup/, redirect_to_url)
|
||||
end
|
||||
|
||||
def authenticate_via_launchpad_as(identity)
|
||||
get signup_session_url, headers: auth_headers, params: { sig: identity.perishable_signature }
|
||||
assert_equal identity.id, session[:signup]["identity_id"]
|
||||
assert_redirected_to new_signup_completion_url
|
||||
post signup_completions_url, headers: auth_headers
|
||||
end
|
||||
|
||||
def assert_redirected_to_account(signal_account = SignalId::Account.last)
|
||||
assert_response :redirect
|
||||
assert_match(/#{Regexp.escape(signal_account.url("/session/launchpad"))}.*&sig=/, redirect_to_url)
|
||||
end
|
||||
end
|
||||
Vendored
+1
@@ -1,3 +1,4 @@
|
||||
37s:
|
||||
name: 37signals
|
||||
join_code: "ejpP-THlQ-Cc2f"
|
||||
queenbee_id: <%= ActiveRecord::FixtureSet.identify :'37s_fizzy' %>
|
||||
|
||||
Vendored
+3
@@ -5,18 +5,21 @@ david:
|
||||
email_address: david@37signals.com
|
||||
password_digest: <%= digest %>
|
||||
role: member
|
||||
signal_user: 37s_fizzy_david
|
||||
|
||||
jz:
|
||||
name: JZ
|
||||
email_address: jz@37signals.com
|
||||
password_digest: <%= digest %>
|
||||
role: member
|
||||
signal_user: 37s_fizzy_jzimdars
|
||||
|
||||
kevin:
|
||||
name: Kevin
|
||||
email_address: kevin@37signals.com
|
||||
password_digest: <%= digest %>
|
||||
role: admin
|
||||
signal_user: 37s_fizzy_kevin
|
||||
|
||||
system:
|
||||
name: System
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
require "test_helper"
|
||||
|
||||
class Account::SignalAccountTest < ActiveSupport::TestCase
|
||||
# # TODO(MIKE): Queenbee client API compliance tests
|
||||
# include Queenbee::Testing::Client
|
||||
|
||||
setup do
|
||||
@account = accounts("37s")
|
||||
end
|
||||
|
||||
test "belongs to a signal_account via a shared queenbee_id" do
|
||||
assert_not_nil @account.queenbee_id
|
||||
assert_equal @account.queenbee_id, Account.new(signal_account: @account.signal_account).queenbee_id
|
||||
assert_equal @account.signal_account, Account.new(queenbee_id: @account.queenbee_id).signal_account
|
||||
end
|
||||
|
||||
test ".create_with_admin_user creates a new local account and user peers" do
|
||||
ApplicationRecord.create_tenant("account-create-with-dependents") do
|
||||
signal_account = signal_accounts(:honcho_fizzy)
|
||||
account = Account.create_with_admin_user(queenbee_id: signal_account.queenbee_id)
|
||||
|
||||
assert_not_nil account
|
||||
assert account.persisted?
|
||||
assert_equal 1, Account.count
|
||||
assert_equal signal_account.queenbee_id, account.queenbee_id
|
||||
assert_equal signal_account.name, account.name
|
||||
assert_equal account, signal_account.peer
|
||||
|
||||
assert_equal 1, User.count
|
||||
User.first.tap do |user|
|
||||
assert signal_account.owner.name, user.name
|
||||
assert signal_account.owner.email_address, user.email_address
|
||||
assert signal_account.owner.id, user.signal_user_id
|
||||
assert_equal "admin", user.role
|
||||
assert_equal user, signal_account.owner.peer
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,4 @@
|
||||
require "test_helper"
|
||||
|
||||
class OrganizationTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
class AccountTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
require "test_helper"
|
||||
|
||||
class SignupTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@signup = Signup.new(
|
||||
email_address: "brian@example.com",
|
||||
full_name: "Brian Wilson",
|
||||
company_name: "Beach Boys",
|
||||
password: SecureRandom.hex(16)
|
||||
)
|
||||
end
|
||||
|
||||
test "#to_h allows persistence of the signup data" do
|
||||
actual = @signup.to_h
|
||||
expected = {
|
||||
email_address: @signup.email_address,
|
||||
full_name: @signup.full_name,
|
||||
company_name: @signup.company_name
|
||||
}
|
||||
assert_equal expected, actual
|
||||
end
|
||||
|
||||
test "#process creates all the necessary objects for a new identity" do
|
||||
Account.any_instance.expects(:setup_basic_template).once
|
||||
|
||||
assert @signup.process, @signup.errors.full_messages.to_sentence(words_connector: ". ")
|
||||
assert_empty @signup.errors
|
||||
|
||||
assert @signup.signal_identity
|
||||
assert @signup.signal_identity.persisted?
|
||||
|
||||
assert @signup.queenbee_account
|
||||
assert @signup.queenbee_account.id
|
||||
|
||||
assert @signup.signal_account
|
||||
assert @signup.signal_account.persisted?
|
||||
assert_equal @signup.signal_identity, @signup.signal_account.owner.identity
|
||||
assert_equal @signup.company_name, @signup.signal_account.name
|
||||
|
||||
assert @signup.account
|
||||
assert @signup.account.persisted?
|
||||
|
||||
assert @signup.user
|
||||
assert @signup.user.persisted?
|
||||
assert_equal @signup.user.signal_user, @signup.signal_account.owner
|
||||
|
||||
assert_includes ApplicationRecord.tenants, @signup.signal_account.subdomain
|
||||
end
|
||||
|
||||
test "the new account is named with company name if present" do
|
||||
assert_equal @signup.company_name, @signup.send(:queenbee_account_name)
|
||||
end
|
||||
|
||||
test "in beta, the new account is named distinctly" do
|
||||
Rails.stubs(:env).returns(stub(beta?: true))
|
||||
|
||||
assert_equal "#{@signup.company_name} (Beta)", @signup.send(:queenbee_account_name)
|
||||
end
|
||||
|
||||
test "#process creates all the necessary objects for an existing identity" do
|
||||
signal_identity = SignalId::Identity.find_by_email_address("david@37signals.com")
|
||||
|
||||
@signup = Signup.new(
|
||||
signal_identity: signal_identity,
|
||||
company_name: "Team LeMans",
|
||||
)
|
||||
|
||||
Account.any_instance.expects(:setup_basic_template).once
|
||||
|
||||
assert @signup.process, @signup.errors.full_messages.to_sentence(words_connector: ". ")
|
||||
assert_empty @signup.errors
|
||||
|
||||
assert @signup.signal_identity
|
||||
assert_equal signal_identity, @signup.signal_identity
|
||||
|
||||
assert @signup.queenbee_account
|
||||
assert @signup.queenbee_account.id
|
||||
|
||||
assert @signup.signal_account
|
||||
assert @signup.signal_account.persisted?
|
||||
assert_equal @signup.signal_identity, @signup.signal_account.owner.identity
|
||||
|
||||
assert @signup.account
|
||||
assert @signup.account.persisted?
|
||||
|
||||
assert @signup.user
|
||||
assert @signup.user.persisted?
|
||||
assert_equal @signup.user.signal_user, @signup.signal_account.owner
|
||||
|
||||
assert_includes ApplicationRecord.tenants, @signup.signal_account.subdomain
|
||||
end
|
||||
|
||||
test "#process does nothing if a validation error occurs creating identity" do
|
||||
@signup.password = ""
|
||||
|
||||
assert_not @signup.process
|
||||
assert_not_empty @signup.errors[:password]
|
||||
|
||||
assert_nil @signup.signal_identity
|
||||
assert_nil @signup.queenbee_account
|
||||
assert_nil @signup.account
|
||||
assert_nil @signup.user
|
||||
end
|
||||
|
||||
test "#process does nothing if a validation error occurs creating the queenbee account" do
|
||||
Queenbee::Remote::Account.stubs(:create!).raises(RuntimeError, "Invalid account data")
|
||||
|
||||
SignalId::Identity.any_instance.expects(:destroy).once
|
||||
|
||||
assert_not @signup.process
|
||||
assert_not_empty @signup.errors[:base]
|
||||
|
||||
assert_nil @signup.signal_identity
|
||||
assert_nil @signup.queenbee_account
|
||||
assert_nil @signup.account
|
||||
assert_nil @signup.user
|
||||
end
|
||||
|
||||
test "#process does nothing if a validation error occurs creating the tenant" do
|
||||
ApplicationRecord.stubs(:create_tenant).raises(RuntimeError, "Tenant already exists")
|
||||
|
||||
Queenbee::Remote::Account.any_instance.expects(:cancel).once
|
||||
SignalId::Identity.any_instance.expects(:destroy).once
|
||||
|
||||
assert_not @signup.process
|
||||
assert_not_empty @signup.errors[:base]
|
||||
|
||||
assert_nil @signup.signal_identity
|
||||
assert_nil @signup.queenbee_account
|
||||
assert_nil @signup.account
|
||||
assert_nil @signup.user
|
||||
end
|
||||
|
||||
test "#process does nothing if a validation error occurs creating the account" do
|
||||
Account.stubs(:create_with_admin_user).raises(RuntimeError, "Account creation failed")
|
||||
|
||||
ApplicationRecord.expects(:destroy_tenant).once
|
||||
Queenbee::Remote::Account.any_instance.expects(:cancel).once
|
||||
SignalId::Identity.any_instance.expects(:destroy).once
|
||||
|
||||
assert_not @signup.process
|
||||
assert_not_empty @signup.errors[:base]
|
||||
|
||||
assert_nil @signup.signal_identity
|
||||
assert_nil @signup.queenbee_account
|
||||
assert_nil @signup.account
|
||||
assert_nil @signup.user
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
require "test_helper"
|
||||
|
||||
class User::SignalUserTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@user = users(:david)
|
||||
end
|
||||
|
||||
test "belongs to a Signal::User" do
|
||||
assert_not_nil @user.signal_user_id
|
||||
assert_equal signal_users("37s_fizzy_david"), @user.signal_user
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,9 @@ require_relative "../config/environment"
|
||||
require "rails/test_help"
|
||||
require "webmock/minitest"
|
||||
require "vcr"
|
||||
require "signal_id/testing"
|
||||
require "queenbee/testing/mocks"
|
||||
require "mocha/minitest"
|
||||
|
||||
WebMock.allow_net_connect!
|
||||
|
||||
@@ -25,6 +28,7 @@ module ActiveSupport
|
||||
fixtures :all
|
||||
|
||||
include ActiveJob::TestHelper
|
||||
include SignalId::Testing
|
||||
include ActionTextTestHelper, CardTestHelper, ChangeTestHelper, SessionTestHelper
|
||||
end
|
||||
end
|
||||
@@ -32,3 +36,11 @@ end
|
||||
RubyLLM.configure do |config|
|
||||
config.openai_api_key ||= "DUMMY-TEST-KEY" # Run tests with VCR without having to configure OpenAI API key locally.
|
||||
end
|
||||
|
||||
Queenbee::Remote::Account.class_eval do
|
||||
# because we use the account ID as the tenant name, we need it to be unique in each test to avoid
|
||||
# parallelized tests clobbering each other.
|
||||
def next_id
|
||||
super + Random.rand(1000000)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user