Initial commit: Move fizzy-saas to separate repository
This commit is contained in:
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: bundler
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 10
|
||||||
Vendored
+69
@@ -0,0 +1,69 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
RUBY_VERSION: ruby-3.4.5
|
||||||
|
RUBOCOP_CACHE_ROOT: tmp/rubocop
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Set up Ruby
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: ${{ env.RUBY_VERSION }}
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- name: Prepare RuboCop cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
DEPENDENCIES_HASH: ${{ hashFiles('**/.rubocop.yml', '**/.rubocop_todo.yml', 'Gemfile.lock') }}
|
||||||
|
with:
|
||||||
|
path: ${{ env.RUBOCOP_CACHE_ROOT }}
|
||||||
|
key: rubocop-${{ runner.os }}-${{ env.RUBY_VERSION }}-${{ env.DEPENDENCIES_HASH }}-${{ github.ref_name == github.event.repository.default_branch && github.run_id || 'default' }}
|
||||||
|
restore-keys: |
|
||||||
|
rubocop-${{ runner.os }}-${{ env.RUBY_VERSION }}-${{ env.DEPENDENCIES_HASH }}-
|
||||||
|
|
||||||
|
- name: Lint code for consistent style
|
||||||
|
run: bin/rubocop -f github
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
# services:
|
||||||
|
# redis:
|
||||||
|
# image: valkey/valkey:8
|
||||||
|
# ports:
|
||||||
|
# - 6379:6379
|
||||||
|
# options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Set up Ruby
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: ruby-3.4.5
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
env:
|
||||||
|
RAILS_ENV: test
|
||||||
|
# RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
|
||||||
|
# REDIS_URL: redis://localhost:6379/0
|
||||||
|
run: bin/rails db:test:prepare test
|
||||||
|
|
||||||
|
- name: Keep screenshots from failed system tests
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: screenshots
|
||||||
|
path: ${{ github.workspace }}/tmp/screenshots
|
||||||
|
if-no-files-found: ignore
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/.bundle/
|
||||||
|
/doc/
|
||||||
|
/log/*.log
|
||||||
|
/pkg/
|
||||||
|
/tmp/
|
||||||
|
/test/dummy/db/*.sqlite3
|
||||||
|
/test/dummy/db/*.sqlite3-*
|
||||||
|
/test/dummy/log/*.log
|
||||||
|
/test/dummy/storage/
|
||||||
|
/test/dummy/tmp/
|
||||||
Executable
+17
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
MESSAGE="$KAMAL_PERFORMER deployed $KAMAL_SERVICE_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds"
|
||||||
|
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
|
||||||
|
bin/notify_dash_of_deployment "$MESSAGE" $KAMAL_VERSION $KAMAL_PERFORMER $CURRENT_BRANCH $KAMAL_DESTINATION $KAMAL_RUNTIME
|
||||||
|
|
||||||
|
if [[ $CURRENT_BRANCH == "main" && $KAMAL_DESTINATION == "production" ]]; then
|
||||||
|
gh release create $KAMAL_SERVICE_VERSION --target $KAMAL_VERSION --generate-notes 2> /dev/null || true
|
||||||
|
|
||||||
|
RELEASE_URL=$(gh release view $KAMAL_SERVICE_VERSION --json url,body --jq .url)
|
||||||
|
RELEASE_BODY=$(gh release view $KAMAL_SERVICE_VERSION --json url,body --jq .body)
|
||||||
|
|
||||||
|
bin/broadcast_to_bc "$MESSAGE "$'\n'"$RELEASE_URL "$'\n'"$RELEASE_BODY"
|
||||||
|
else
|
||||||
|
bin/broadcast_to_bc "$MESSAGE"
|
||||||
|
fi
|
||||||
Executable
+82
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Validate hostnames are FQDNs ending in -int.37signals.com
|
||||||
|
if command -v yq >/dev/null 2>&1; then
|
||||||
|
declare -A SUGGESTIONS
|
||||||
|
while IFS= read -r host; do
|
||||||
|
if [[ ! $host =~ -int\.37signals\.com$ ]]; then
|
||||||
|
if [[ $host =~ -4[0-9]{2}$ ]]; then
|
||||||
|
SUGGESTIONS["$host"]="$host.df-ams-int.37signals.com"
|
||||||
|
elif [[ $host =~ -1[0-9]{2}$ ]]; then
|
||||||
|
SUGGESTIONS["$host"]="$host.df-iad-int.37signals.com"
|
||||||
|
else
|
||||||
|
SUGGESTIONS["$host"]="$host.sc-chi-int.37signals.com"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < <(bin/kamal config -d "${KAMAL_DESTINATION:-production}" 2>/dev/null | yq -r '.":hosts"[]')
|
||||||
|
|
||||||
|
if [ ${#SUGGESTIONS[@]} -gt 0 ]; then
|
||||||
|
echo "Unqualified hostnames found in config/deploy.${KAMAL_DESTINATION:-production}.yml:" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "Update to use fully-qualified hostnames:" >&2
|
||||||
|
for host in "${!SUGGESTIONS[@]}"; do
|
||||||
|
echo " $host → ${SUGGESTIONS[$host]}" >&2
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify Tailscale connection and SSH authentication before deploying.
|
||||||
|
tailscale_cmd() {
|
||||||
|
if command -v tailscale >/dev/null 2>&1; then
|
||||||
|
tailscale "$@"
|
||||||
|
elif [ -f "/Applications/Tailscale.app/Contents/MacOS/Tailscale" ]; then
|
||||||
|
env TAILSCALE_BE_CLI=1 /Applications/Tailscale.app/Contents/MacOS/Tailscale "$@"
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
on_tailscale() {
|
||||||
|
tailscale_cmd status --json 2>/dev/null | jq -e '.Self.Online' >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check Tailscale connection
|
||||||
|
if ! on_tailscale; then
|
||||||
|
echo "" >&2
|
||||||
|
echo "You must be connected to Tailscale to deploy." >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "→ Connect to Tailscale and try again" >&2
|
||||||
|
echo "" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify SSH access
|
||||||
|
echo "Deploying via Tailscale. Verifying SSH access…" >&2
|
||||||
|
|
||||||
|
TEST_HOST="fizzy-app-101"
|
||||||
|
|
||||||
|
SSH_OUTPUT=$(ssh -o ConnectTimeout=5 "app@$TEST_HOST" true 2>&1)
|
||||||
|
SSH_EXIT=$?
|
||||||
|
|
||||||
|
echo "$SSH_OUTPUT" >&2
|
||||||
|
|
||||||
|
if echo "$SSH_OUTPUT" | grep -q "Permission denied"; then
|
||||||
|
GITHUB_USER=$(gh api user 2>/dev/null | jq -r '.login // "unknown"')
|
||||||
|
GITHUB_KEYS_URL="https://github.com/${GITHUB_USER}.keys"
|
||||||
|
|
||||||
|
echo "" >&2
|
||||||
|
echo "ERROR: SSH authentication failed" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "You must deploy with an SSH key that's on your GitHub account." >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "→ Verify your public key is at $GITHUB_KEYS_URL" >&2
|
||||||
|
echo " Add it at https://github.com/settings/keys if not" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "Note that SSH keys are pulled from GitHub every 5 minutes, so if you've" >&2
|
||||||
|
echo "just added a new key to GitHub, try again in five." >&2
|
||||||
|
echo "" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $SSH_EXIT
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Omakase Ruby styling for Rails
|
||||||
|
inherit_gem: { rubocop-rails-omakase: rubocop.yml }
|
||||||
|
|
||||||
|
# Overwrite or add rules to create your own house style
|
||||||
|
#
|
||||||
|
# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]`
|
||||||
|
# Layout/SpaceInsideArrayLiteralBrackets:
|
||||||
|
# Enabled: false
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
source "https://rubygems.org"
|
||||||
|
git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
|
||||||
|
|
||||||
|
# 37id and Queenbee integration
|
||||||
|
gem "queenbee", bc: "queenbee-plugin", ref: "eb01c697de1ad028afc65cc7d9b5345a7a8e849f"
|
||||||
|
gem "activeresource", require: "active_resource" # needed by queenbee
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
GIT
|
||||||
|
remote: https://github.com/basecamp/queenbee-plugin
|
||||||
|
revision: eb01c697de1ad028afc65cc7d9b5345a7a8e849f
|
||||||
|
ref: eb01c697de1ad028afc65cc7d9b5345a7a8e849f
|
||||||
|
specs:
|
||||||
|
queenbee (3.2.0)
|
||||||
|
activeresource
|
||||||
|
builder
|
||||||
|
rexml
|
||||||
|
|
||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
activemodel (8.0.2.1)
|
||||||
|
activesupport (= 8.0.2.1)
|
||||||
|
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)
|
||||||
|
activesupport (8.0.2.1)
|
||||||
|
base64
|
||||||
|
benchmark (>= 0.3)
|
||||||
|
bigdecimal
|
||||||
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||||
|
connection_pool (>= 2.2.5)
|
||||||
|
drb
|
||||||
|
i18n (>= 1.6, < 2)
|
||||||
|
logger (>= 1.4.2)
|
||||||
|
minitest (>= 5.1)
|
||||||
|
securerandom (>= 0.3)
|
||||||
|
tzinfo (~> 2.0, >= 2.0.5)
|
||||||
|
uri (>= 0.13.1)
|
||||||
|
base64 (0.3.0)
|
||||||
|
benchmark (0.4.1)
|
||||||
|
bigdecimal (3.2.3)
|
||||||
|
builder (3.3.0)
|
||||||
|
concurrent-ruby (1.3.5)
|
||||||
|
connection_pool (2.5.4)
|
||||||
|
drb (2.2.3)
|
||||||
|
i18n (1.14.7)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
logger (1.7.0)
|
||||||
|
minitest (5.25.5)
|
||||||
|
rexml (3.4.4)
|
||||||
|
securerandom (0.4.1)
|
||||||
|
tzinfo (2.0.6)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
uri (1.0.3)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
aarch64-linux-gnu
|
||||||
|
aarch64-linux-musl
|
||||||
|
arm-linux-gnu
|
||||||
|
arm-linux-musl
|
||||||
|
arm64-darwin
|
||||||
|
x86_64-darwin
|
||||||
|
x86_64-linux-gnu
|
||||||
|
x86_64-linux-musl
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
activeresource
|
||||||
|
queenbee!
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.7.0
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Fizzy::Saas
|
||||||
|
Short description and motivation.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
How to use my plugin.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
Add this line to your application's Gemfile:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
gem "fizzy-saas"
|
||||||
|
```
|
||||||
|
|
||||||
|
And then execute:
|
||||||
|
```bash
|
||||||
|
$ bundle
|
||||||
|
```
|
||||||
|
|
||||||
|
Or install it yourself as:
|
||||||
|
```bash
|
||||||
|
$ gem install fizzy-saas
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
Contribution directions go here.
|
||||||
|
|
||||||
|
## License
|
||||||
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
require "bundler/setup"
|
||||||
|
|
||||||
|
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
||||||
|
load "rails/tasks/engine.rake"
|
||||||
|
|
||||||
|
load "rails/tasks/statistics.rake"
|
||||||
|
|
||||||
|
require "bundler/gem_tasks"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
||||||
|
* listed below.
|
||||||
|
*
|
||||||
|
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
||||||
|
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
||||||
|
*
|
||||||
|
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
||||||
|
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
||||||
|
* files in this directory. Styles in this file should be added after the last require_* statement.
|
||||||
|
* It is generally better to create a new file per style scope.
|
||||||
|
*
|
||||||
|
*= require_tree .
|
||||||
|
*= require_self
|
||||||
|
*/
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
class Signup::CompletionsController < ApplicationController
|
||||||
|
layout "public"
|
||||||
|
|
||||||
|
disallow_account_scope
|
||||||
|
|
||||||
|
def new
|
||||||
|
@signup = Signup.new(identity: Current.identity)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@signup = Signup.new(signup_params)
|
||||||
|
|
||||||
|
if @signup.complete
|
||||||
|
redirect_to landing_url(script_name: @signup.account.slug)
|
||||||
|
else
|
||||||
|
render :new, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def signup_params
|
||||||
|
params.expect(signup: %i[ full_name ]).with_defaults(identity: Current.identity)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
class Signup
|
||||||
|
include ActiveModel::Model
|
||||||
|
include ActiveModel::Attributes
|
||||||
|
include ActiveModel::Validations
|
||||||
|
|
||||||
|
attr_accessor :full_name, :email_address, :identity
|
||||||
|
attr_reader :queenbee_account, :account, :user
|
||||||
|
|
||||||
|
with_options on: :completion do
|
||||||
|
validates_presence_of :full_name, :identity
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(...)
|
||||||
|
@full_name = nil
|
||||||
|
@email_address = nil
|
||||||
|
@account = nil
|
||||||
|
@user = nil
|
||||||
|
@queenbee_account = nil
|
||||||
|
@identity = nil
|
||||||
|
|
||||||
|
super
|
||||||
|
|
||||||
|
@email_address = @identity.email_address if @identity
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_identity
|
||||||
|
@identity = Identity.find_or_create_by!(email_address: email_address)
|
||||||
|
@identity.send_magic_link
|
||||||
|
end
|
||||||
|
|
||||||
|
def complete
|
||||||
|
if valid?(:completion)
|
||||||
|
begin
|
||||||
|
create_queenbee_account
|
||||||
|
create_account
|
||||||
|
|
||||||
|
true
|
||||||
|
rescue => error
|
||||||
|
destroy_account
|
||||||
|
destroy_queenbee_account
|
||||||
|
|
||||||
|
errors.add(:base, "Something went wrong, and we couldn't create your account. Please give it another try.")
|
||||||
|
Rails.error.report(error, severity: :error)
|
||||||
|
Rails.logger.error error
|
||||||
|
Rails.logger.error error.backtrace.join("\n")
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def create_queenbee_account
|
||||||
|
@account_name = AccountNameGenerator.new(identity: identity, name: full_name).generate
|
||||||
|
@queenbee_account = Queenbee::Remote::Account.create!(queenbee_account_attributes)
|
||||||
|
@tenant = queenbee_account.id.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy_queenbee_account
|
||||||
|
@queenbee_account&.cancel
|
||||||
|
@queenbee_account = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_account
|
||||||
|
@account = Account.create_with_admin_user(
|
||||||
|
account: {
|
||||||
|
external_account_id: @tenant,
|
||||||
|
name: @account_name
|
||||||
|
},
|
||||||
|
owner: {
|
||||||
|
name: full_name,
|
||||||
|
identity: identity
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@user = @account.users.find_by!(role: :admin)
|
||||||
|
@account.setup_customer_template
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy_account
|
||||||
|
@account&.destroy!
|
||||||
|
|
||||||
|
@user = nil
|
||||||
|
@account = nil
|
||||||
|
@tenant = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def queenbee_account_attributes
|
||||||
|
{}.tap do |attributes|
|
||||||
|
attributes[:product_name] = "fizzy"
|
||||||
|
attributes[:name] = @account_name
|
||||||
|
attributes[:owner_name] = full_name
|
||||||
|
attributes[:owner_email] = email_address
|
||||||
|
|
||||||
|
attributes[:trial] = true
|
||||||
|
attributes[:subscription] = subscription_attributes
|
||||||
|
attributes[:remote_request] = request_attributes
|
||||||
|
|
||||||
|
# # TODO: Terms of Service
|
||||||
|
# attributes[:terms_of_service] = true
|
||||||
|
|
||||||
|
# We've confirmed the email
|
||||||
|
attributes[:auto_allow] = true
|
||||||
|
|
||||||
|
# Tell Queenbee to skip the request to create a local account. We've created it ourselves.
|
||||||
|
attributes[:skip_remote] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def subscription_attributes
|
||||||
|
subscription = FreeV1Subscription
|
||||||
|
|
||||||
|
{}.tap do |attributes|
|
||||||
|
attributes[:name] = subscription.to_param
|
||||||
|
attributes[:price] = subscription.price
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def request_attributes
|
||||||
|
{}.tap do |attributes|
|
||||||
|
attributes[:remote_address] = Current.ip_address
|
||||||
|
attributes[:user_agent] = Current.user_agent
|
||||||
|
attributes[:referrer] = Current.referrer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
class Signup::AccountNameGenerator
|
||||||
|
SUFFIX = "Fizzy".freeze
|
||||||
|
|
||||||
|
attr_reader :identity, :name
|
||||||
|
|
||||||
|
def initialize(identity:, name:)
|
||||||
|
@identity = identity
|
||||||
|
@name = name
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate
|
||||||
|
next_index = current_index + 1
|
||||||
|
|
||||||
|
if next_index == 1
|
||||||
|
"#{prefix} #{SUFFIX}"
|
||||||
|
else
|
||||||
|
"#{prefix} #{next_index.ordinalize} #{SUFFIX}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def current_index
|
||||||
|
existing_indices.max || 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def existing_indices
|
||||||
|
Current.without_account do
|
||||||
|
identity.accounts.filter_map do |account|
|
||||||
|
if account.name.match?(first_account_name_regex)
|
||||||
|
1
|
||||||
|
elsif match = account.name.match(nth_account_name_regex)
|
||||||
|
match[1].to_i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def first_account_name_regex
|
||||||
|
@first_account_name_regex ||= /\A#{prefix}\s+#{SUFFIX}\Z/i
|
||||||
|
end
|
||||||
|
|
||||||
|
def nth_account_name_regex
|
||||||
|
@nth_account_name_regex ||= /\A#{prefix}\s+(1st|2nd|3rd|\d+th)\s+#{SUFFIX}/i
|
||||||
|
end
|
||||||
|
|
||||||
|
def prefix
|
||||||
|
@prefix ||= "#{first_name}'s"
|
||||||
|
end
|
||||||
|
|
||||||
|
def first_name
|
||||||
|
name.strip.split(" ", 2).first
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
class Subscription < Queenbee::Subscription
|
||||||
|
SHORT_NAMES = %w[ FreeV1 ]
|
||||||
|
|
||||||
|
def self.short_name
|
||||||
|
name.demodulize
|
||||||
|
end
|
||||||
|
|
||||||
|
class FreeV1 < Subscription
|
||||||
|
property :proper_name, "Free Subscription"
|
||||||
|
property :price, 0
|
||||||
|
property :frequency, "yearly"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Fizzy saas</title>
|
||||||
|
<%= csrf_meta_tags %>
|
||||||
|
<%= csp_meta_tag %>
|
||||||
|
|
||||||
|
<%= yield :head %>
|
||||||
|
|
||||||
|
<%= stylesheet_link_tag "fizzy/saas/application", media: "all" %>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<%= yield %>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<% @page_title = "Complete your sign-up" %>
|
||||||
|
|
||||||
|
<div class="panel panel--centered flex flex-column gap-half <%= "shake" if flash[:alert] %>">
|
||||||
|
<h1 class="txt-x-large font-weight-black margin-block-end"><%= @page_title %></h1>
|
||||||
|
|
||||||
|
<%= form_with model: @signup, url: saas.signup_completion_path, scope: "signup", class: "flex flex-column gap", data: { controller: "form" } do |form| %>
|
||||||
|
<%= form.text_field :full_name, class: "input txt-large", autocomplete: "name", placeholder: "Enter your full name…", autofocus: true, required: true %>
|
||||||
|
|
||||||
|
<p>You’re one step away. Just enter your name to get your own Fizzy account.</p>
|
||||||
|
|
||||||
|
<% if @signup.errors.any? %>
|
||||||
|
<div class="margin-block-half">
|
||||||
|
<ul class="margin-block-none txt-negative txt-small">
|
||||||
|
<% @signup.errors.full_messages.each do |message| %>
|
||||||
|
<li><%= message %></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn--link center" data-form-target="submit">
|
||||||
|
<span>Continue</span>
|
||||||
|
<%= icon_tag "arrow-right" %>
|
||||||
|
</button>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% content_for :footer do %>
|
||||||
|
<%= render "sessions/footer" %>
|
||||||
|
<% end %>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<% @page_title = "Sign up for Fizzy" %>
|
||||||
|
|
||||||
|
<div class="panel panel--centered flex flex-column gap-half <%= "shake" if flash[:alert] %>">
|
||||||
|
<h1 class="txt-xx-large margin-block-end-double">Sign up</h1>
|
||||||
|
|
||||||
|
<%= form_with model: @signup, url: saas.signup_path, scope: "signup", class: "flex flex-column gap", data: { turbo: false, controller: "form" } do |form| %>
|
||||||
|
<%= form.email_field :email_address, class: "input", autocomplete: "username", placeholder: "Email address", required: true %>
|
||||||
|
|
||||||
|
<% if @signup.errors.any? %>
|
||||||
|
<div class="margin-block-half">
|
||||||
|
<ul class="margin-block-none txt-negative txt-small">
|
||||||
|
<% @signup.errors.full_messages.each do |message| %>
|
||||||
|
<li><%= message %></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn--link center" data-form-target="submit">
|
||||||
|
<span>Continue</span>
|
||||||
|
<%= icon_tag "arrow-right" %>
|
||||||
|
</button>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% content_for :footer do %>
|
||||||
|
<%= render "sessions/footer" %>
|
||||||
|
<% end %>
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# This command will automatically be run when you run "rails" with Rails gems
|
||||||
|
# installed from the root of your application.
|
||||||
|
|
||||||
|
ENGINE_ROOT = File.expand_path("..", __dir__)
|
||||||
|
ENGINE_PATH = File.expand_path("../lib/fizzy/saas/engine", __dir__)
|
||||||
|
APP_PATH = File.expand_path("../test/dummy/config/application", __dir__)
|
||||||
|
|
||||||
|
# Set up gems listed in the Gemfile.
|
||||||
|
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
||||||
|
require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
|
||||||
|
|
||||||
|
require "rails/all"
|
||||||
|
require "rails/engine/commands"
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
require "rubygems"
|
||||||
|
require "bundler/setup"
|
||||||
|
|
||||||
|
# explicit rubocop config increases performance slightly while avoiding config confusion.
|
||||||
|
ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))
|
||||||
|
|
||||||
|
load Gem.bin_path("rubocop", "rubocop")
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<%
|
||||||
|
if ENV["MIGRATE"].present?
|
||||||
|
mysql_app_user_key = "MYSQL_ALTER_USER"
|
||||||
|
mysql_app_password_key = "MYSQL_ALTER_PASSWORD"
|
||||||
|
else
|
||||||
|
mysql_app_user_key = "MYSQL_APP_USER"
|
||||||
|
mysql_app_password_key = "MYSQL_APP_PASSWORD"
|
||||||
|
end
|
||||||
|
|
||||||
|
mysql_app_user = ENV[mysql_app_user_key]
|
||||||
|
mysql_app_password = ENV[mysql_app_password_key]
|
||||||
|
%>
|
||||||
|
|
||||||
|
default: &default
|
||||||
|
adapter: trilogy
|
||||||
|
host: <%= ENV.fetch "FIZZY_DB_HOST", "127.0.0.1" %>
|
||||||
|
port: <%= ENV.fetch "FIZZY_DB_PORT", 3306 %>
|
||||||
|
pool: 50
|
||||||
|
timeout: 5000
|
||||||
|
variables:
|
||||||
|
transaction_isolation: READ-COMMITTED
|
||||||
|
|
||||||
|
development:
|
||||||
|
primary:
|
||||||
|
<<: *default
|
||||||
|
database: fizzy_development
|
||||||
|
port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %>
|
||||||
|
replica:
|
||||||
|
<<: *default
|
||||||
|
database: fizzy_development
|
||||||
|
port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %>
|
||||||
|
replica: true
|
||||||
|
cable:
|
||||||
|
<<: *default
|
||||||
|
database: development_cable
|
||||||
|
port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %>
|
||||||
|
migrations_paths: db/cable_migrate
|
||||||
|
cache:
|
||||||
|
<<: *default
|
||||||
|
database: development_cache
|
||||||
|
port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %>
|
||||||
|
migrations_paths: db/cache_migrate
|
||||||
|
queue:
|
||||||
|
<<: *default
|
||||||
|
database: development_queue
|
||||||
|
port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %>
|
||||||
|
migrations_paths: db/queue_migrate
|
||||||
|
|
||||||
|
# Warning: The database defined as "test" will be erased and
|
||||||
|
# re-generated from your development database when you run "rake".
|
||||||
|
# Do not set this db to the same as development or production.
|
||||||
|
test:
|
||||||
|
primary:
|
||||||
|
<<: *default
|
||||||
|
database: fizzy_test
|
||||||
|
port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %>
|
||||||
|
replica:
|
||||||
|
<<: *default
|
||||||
|
database: fizzy_test
|
||||||
|
port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %>
|
||||||
|
replica: true
|
||||||
|
|
||||||
|
production: &production
|
||||||
|
primary:
|
||||||
|
<<: *default
|
||||||
|
database: fizzy_production
|
||||||
|
host: <%= ENV["MYSQL_DATABASE_HOST"] %>
|
||||||
|
username: <%= mysql_app_user %>
|
||||||
|
password: <%= mysql_app_password %>
|
||||||
|
replica:
|
||||||
|
<<: *default
|
||||||
|
database: fizzy_production
|
||||||
|
host: <%= ENV["MYSQL_DATABASE_REPLICA_HOST"] %>
|
||||||
|
username: <%= ENV["MYSQL_READONLY_USER"] %>
|
||||||
|
password: <%= ENV["MYSQL_READONLY_PASSWORD"] %>
|
||||||
|
replica: true
|
||||||
|
cable:
|
||||||
|
<<: *default
|
||||||
|
database: fizzy_solidcable_production
|
||||||
|
host: <%= ENV["MYSQL_SOLID_CABLE_HOST"] %>
|
||||||
|
username: <%= mysql_app_user %>
|
||||||
|
password: <%= mysql_app_password %>
|
||||||
|
migrations_paths: db/cable_migrate
|
||||||
|
queue:
|
||||||
|
<<: *default
|
||||||
|
database: fizzy_solidqueue_production
|
||||||
|
host: <%= ENV["MYSQL_SOLID_QUEUE_HOST"] %>
|
||||||
|
username: <%= mysql_app_user %>
|
||||||
|
password: <%= mysql_app_password %>
|
||||||
|
migrations_paths: db/queue_migrate
|
||||||
|
cache:
|
||||||
|
<<: *default
|
||||||
|
database: fizzy_solidcache_production
|
||||||
|
host: <%= ENV["MYSQL_SOLID_CACHE_HOST"] %>
|
||||||
|
username: <%= mysql_app_user %>
|
||||||
|
password: <%= mysql_app_password %>
|
||||||
|
migrations_paths: db/cache_migrate
|
||||||
|
|
||||||
|
beta: *production
|
||||||
|
staging: *production
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
servers:
|
||||||
|
web:
|
||||||
|
hosts:
|
||||||
|
- fizzy-beta-app-01.sc-chi-int.37signals.com
|
||||||
|
- fizzy-beta-app-101.df-iad-int.37signals.com
|
||||||
|
labels:
|
||||||
|
otel_scrape_enabled: true
|
||||||
|
jobs:
|
||||||
|
hosts:
|
||||||
|
- fizzy-beta-app-01.sc-chi-int.37signals.com
|
||||||
|
labels:
|
||||||
|
otel_scrape_enabled: true
|
||||||
|
|
||||||
|
proxy:
|
||||||
|
ssl: false
|
||||||
|
|
||||||
|
ssh:
|
||||||
|
user: app
|
||||||
|
|
||||||
|
env:
|
||||||
|
clear:
|
||||||
|
RAILS_ENV: beta
|
||||||
|
|
||||||
|
accessories:
|
||||||
|
load-balancer:
|
||||||
|
image: basecamp/kamal-proxy:lb
|
||||||
|
host: fizzy-beta-lb-01.sc-chi-int.37signals.com
|
||||||
|
options:
|
||||||
|
publish:
|
||||||
|
- 80:80
|
||||||
|
- 443:443
|
||||||
|
volumes:
|
||||||
|
- load-balancer:/home/kamal-proxy/.config/kamal-proxy
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
servers:
|
||||||
|
web:
|
||||||
|
hosts:
|
||||||
|
- test-1
|
||||||
|
jobs:
|
||||||
|
hosts:
|
||||||
|
- test-1
|
||||||
|
env:
|
||||||
|
clear:
|
||||||
|
ARTENANT: test
|
||||||
|
|
||||||
|
proxy: false
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
servers:
|
||||||
|
web:
|
||||||
|
hosts:
|
||||||
|
- fizzy-app-01.sc-chi-int.37signals.com: sc_chi
|
||||||
|
- fizzy-app-02.sc-chi-int.37signals.com: sc_chi
|
||||||
|
- fizzy-app-101.df-iad-int.37signals.com: df_iad
|
||||||
|
- fizzy-app-102.df-iad-int.37signals.com: df_iad
|
||||||
|
- fizzy-app-401.df-ams-int.37signals.com: df_ams
|
||||||
|
- fizzy-app-402.df-ams-int.37signals.com: df_ams
|
||||||
|
labels:
|
||||||
|
otel_scrape_enabled: true
|
||||||
|
jobs:
|
||||||
|
hosts:
|
||||||
|
- fizzy-jobs-101.df-iad-int.37signals.com: df_iad
|
||||||
|
- fizzy-jobs-102.df-iad-int.37signals.com: df_iad
|
||||||
|
labels:
|
||||||
|
otel_scrape_enabled: true
|
||||||
|
|
||||||
|
proxy:
|
||||||
|
ssl: false
|
||||||
|
|
||||||
|
ssh:
|
||||||
|
user: app
|
||||||
|
|
||||||
|
env:
|
||||||
|
clear:
|
||||||
|
RAILS_ENV: production
|
||||||
|
MYSQL_DATABASE_HOST: fizzy-mysql-primary
|
||||||
|
MYSQL_DATABASE_REPLICA_HOST: fizzy-mysql-replica
|
||||||
|
MYSQL_SOLID_CABLE_HOST: fizzy-mysql-primary
|
||||||
|
MYSQL_SOLID_QUEUE_HOST: fizzy-mysql-primary
|
||||||
|
secret:
|
||||||
|
- RAILS_MASTER_KEY
|
||||||
|
- MYSQL_ALTER_PASSWORD
|
||||||
|
- MYSQL_ALTER_USER
|
||||||
|
- MYSQL_APP_PASSWORD
|
||||||
|
- MYSQL_APP_USER
|
||||||
|
- MYSQL_READONLY_PASSWORD
|
||||||
|
- MYSQL_READONLY_USER
|
||||||
|
tags:
|
||||||
|
sc_chi:
|
||||||
|
MYSQL_SOLID_CACHE_HOST: fizzy-solidcache-db-01.sc-chi-int.37signals.com
|
||||||
|
df_iad:
|
||||||
|
MYSQL_SOLID_CACHE_HOST: fizzy-solidcache-db-101.df-iad-int.37signals.com
|
||||||
|
PRIMARY_DATACENTER: true
|
||||||
|
df_ams:
|
||||||
|
MYSQL_SOLID_CACHE_HOST: fizzy-solidcache-db-401.df-ams-int.37signals.com
|
||||||
|
|
||||||
|
|
||||||
|
accessories:
|
||||||
|
load-balancer:
|
||||||
|
image: basecamp/kamal-proxy:lb
|
||||||
|
hosts:
|
||||||
|
- fizzy-lb-101.df-iad-int.37signals.com
|
||||||
|
- fizzy-lb-01.sc-chi-int.37signals.com
|
||||||
|
- fizzy-lb-401.df-ams-int.37signals.com
|
||||||
|
labels:
|
||||||
|
otel_role: load-balancer
|
||||||
|
otel_service: fizzy-load-balancer
|
||||||
|
otel_scrape_enabled: true
|
||||||
|
options:
|
||||||
|
publish:
|
||||||
|
- 80:80
|
||||||
|
- 443:443
|
||||||
|
# NFS mount for certificates
|
||||||
|
# See https://3.basecamp.com/2914079/buckets/37331921/todos/9180260061
|
||||||
|
mount: type=volume,src=certificates,dst=/certificates,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/fizzy-production-certificates,"volume-opt=o=addr=purestorage.sc-chi-int.37signals.com,nfsvers=3,rw,noatime,nconnect=8,soft,timeo=30,retrans=2"
|
||||||
|
volumes:
|
||||||
|
- load-balancer:/home/kamal-proxy/.config/kamal-proxy
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
servers:
|
||||||
|
web:
|
||||||
|
hosts:
|
||||||
|
- fizzy-staging-app-101.df-iad-int.37signals.com: df_iad
|
||||||
|
- fizzy-staging-app-102.df-iad-int.37signals.com: df_iad
|
||||||
|
- fizzy-staging-app-01.sc-chi-int.37signals.com: sc_chi
|
||||||
|
- fizzy-staging-app-02.sc-chi-int.37signals.com: sc_chi
|
||||||
|
- fizzy-staging-app-401.df-ams-int.37signals.com: df_ams
|
||||||
|
- fizzy-staging-app-402.df-ams-int.37signals.com: df_ams
|
||||||
|
labels:
|
||||||
|
otel_scrape_enabled: true
|
||||||
|
jobs:
|
||||||
|
hosts:
|
||||||
|
- fizzy-staging-jobs-101.df-iad-int.37signals.com: df_iad
|
||||||
|
- fizzy-staging-jobs-102.df-iad-int.37signals.com: df_iad
|
||||||
|
labels:
|
||||||
|
otel_scrape_enabled: true
|
||||||
|
|
||||||
|
proxy:
|
||||||
|
ssl: false
|
||||||
|
|
||||||
|
ssh:
|
||||||
|
user: app
|
||||||
|
|
||||||
|
env:
|
||||||
|
clear:
|
||||||
|
RAILS_ENV: staging
|
||||||
|
MYSQL_DATABASE_HOST: fizzy-staging-mysql-primary
|
||||||
|
MYSQL_DATABASE_REPLICA_HOST: fizzy-staging-mysql-replica
|
||||||
|
MYSQL_SOLID_CABLE_HOST: fizzy-staging-mysql-primary
|
||||||
|
MYSQL_SOLID_QUEUE_HOST: fizzy-staging-mysql-primary
|
||||||
|
secret:
|
||||||
|
- RAILS_MASTER_KEY
|
||||||
|
- MYSQL_ALTER_PASSWORD
|
||||||
|
- MYSQL_ALTER_USER
|
||||||
|
- MYSQL_APP_PASSWORD
|
||||||
|
- MYSQL_APP_USER
|
||||||
|
- MYSQL_READONLY_PASSWORD
|
||||||
|
- MYSQL_READONLY_USER
|
||||||
|
tags:
|
||||||
|
sc_chi:
|
||||||
|
MYSQL_SOLID_CACHE_HOST: fizzy-staging-solidcache-db-01.sc-chi-int.37signals.com
|
||||||
|
df_iad:
|
||||||
|
MYSQL_SOLID_CACHE_HOST: fizzy-staging-solidcache-db-101.df-iad-int.37signals.com
|
||||||
|
PRIMARY_DATACENTER: true
|
||||||
|
df_ams:
|
||||||
|
MYSQL_SOLID_CACHE_HOST: fizzy-staging-solidcache-db-401.df-ams-int.37signals.com
|
||||||
|
|
||||||
|
|
||||||
|
accessories:
|
||||||
|
load-balancer:
|
||||||
|
image: basecamp/kamal-proxy:lb
|
||||||
|
hosts:
|
||||||
|
- fizzy-staging-lb-01.sc-chi-int.37signals.com
|
||||||
|
- fizzy-staging-lb-101.df-iad-int.37signals.com
|
||||||
|
- fizzy-staging-lb-401.df-ams-int.37signals.com
|
||||||
|
labels:
|
||||||
|
otel_role: load-balancer
|
||||||
|
otel_service: fizzy-load-balancer
|
||||||
|
otel_scrape_enabled: true
|
||||||
|
options:
|
||||||
|
publish:
|
||||||
|
- 80:80
|
||||||
|
- 443:443
|
||||||
|
# NFS mount for certificates
|
||||||
|
# See https://3.basecamp.com/2914079/buckets/37331921/todos/9180260061
|
||||||
|
mount: type=volume,src=certificates,dst=/certificates,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/fizzy-staging-certificates,"volume-opt=o=addr=purestorage.sc-chi-int.37signals.com,nfsvers=3,rw,noatime,nconnect=8,soft,timeo=30,retrans=2"
|
||||||
|
volumes:
|
||||||
|
- load-balancer:/home/kamal-proxy/.config/kamal-proxy
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
service: fizzy
|
||||||
|
image: basecamp/fizzy
|
||||||
|
asset_path: /rails/public/assets
|
||||||
|
hooks_path: <%= File.join(Gem::Specification.find_by_name("fizzy-saas").gem_dir, ".kamal", "hooks") %>
|
||||||
|
|
||||||
|
servers:
|
||||||
|
jobs:
|
||||||
|
cmd: bin/jobs
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- fizzy:/rails/storage
|
||||||
|
|
||||||
|
proxy:
|
||||||
|
ssl: true
|
||||||
|
|
||||||
|
registry:
|
||||||
|
server: registry.37signals.com
|
||||||
|
username: robot$harbor-bot
|
||||||
|
password:
|
||||||
|
- BASECAMP_REGISTRY_PASSWORD
|
||||||
|
|
||||||
|
builder:
|
||||||
|
arch: amd64
|
||||||
|
secrets:
|
||||||
|
- GITHUB_TOKEN
|
||||||
|
remote: ssh://app@docker-builder-102
|
||||||
|
local: <%= ENV.fetch("KAMAL_BUILDER_LOCAL", "true") %>
|
||||||
|
|
||||||
|
env:
|
||||||
|
secret:
|
||||||
|
- RAILS_MASTER_KEY
|
||||||
|
|
||||||
|
aliases:
|
||||||
|
console: app exec -i --reuse "bin/rails console"
|
||||||
|
ssh: app exec -i --reuse /bin/bash
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
Fizzy::Saas::Engine.routes.draw do
|
||||||
|
get "/signup/new", to: redirect("/session/new")
|
||||||
|
|
||||||
|
namespace :signup do
|
||||||
|
resource :completion, only: %i[ new create ]
|
||||||
|
end
|
||||||
|
|
||||||
|
Queenbee.routes(self)
|
||||||
|
end
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
require_relative "lib/fizzy/saas/version"
|
||||||
|
|
||||||
|
Gem::Specification.new do |spec|
|
||||||
|
spec.name = "fizzy-saas"
|
||||||
|
spec.version = Fizzy::Saas::VERSION
|
||||||
|
spec.authors = [ "Mike Dalessio" ]
|
||||||
|
spec.email = [ "mike@37signals.com" ]
|
||||||
|
spec.homepage = "TODO"
|
||||||
|
spec.summary = "TODO: Summary of Fizzy::Saas."
|
||||||
|
spec.description = "TODO: Description of Fizzy::Saas."
|
||||||
|
|
||||||
|
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the "allowed_push_host"
|
||||||
|
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
||||||
|
spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
||||||
|
|
||||||
|
spec.metadata["homepage_uri"] = spec.homepage
|
||||||
|
spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
|
||||||
|
spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
||||||
|
|
||||||
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
||||||
|
Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
|
||||||
|
end
|
||||||
|
|
||||||
|
spec.add_dependency "rails", ">= 8.1.0.beta1"
|
||||||
|
spec.add_dependency "queenbee"
|
||||||
|
spec.add_dependency "rails_structured_logging"
|
||||||
|
end
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
require "fizzy/saas/version"
|
||||||
|
require "fizzy/saas/engine"
|
||||||
|
|
||||||
|
module Fizzy
|
||||||
|
module Saas
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
module Fizzy
|
||||||
|
module Saas
|
||||||
|
class Engine < ::Rails::Engine
|
||||||
|
# moved from config/initializers/queenbee.rb
|
||||||
|
Queenbee.host_app = Fizzy
|
||||||
|
|
||||||
|
config.to_prepare do
|
||||||
|
Queenbee::Subscription.short_names = Subscription::SHORT_NAMES
|
||||||
|
Queenbee::ApiToken.token = Rails.application.credentials.dig(:queenbee_api_token)
|
||||||
|
|
||||||
|
Subscription::SHORT_NAMES.each do |short_name|
|
||||||
|
const_name = "#{short_name}Subscription"
|
||||||
|
::Object.send(:remove_const, const_name) if ::Object.const_defined?(const_name)
|
||||||
|
::Object.const_set const_name, Subscription.const_get(short_name, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module Fizzy
|
||||||
|
module Saas
|
||||||
|
VERSION = "0.1.0"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# desc "Explaining what the task does"
|
||||||
|
# task :fizzy_saas do
|
||||||
|
# # Task goes here
|
||||||
|
# end
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class Signup::CompletionsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
setup do
|
||||||
|
@signup = Signup.new(email_address: "newuser@example.com", full_name: "New User")
|
||||||
|
|
||||||
|
@signup.create_identity || raise("Failed to create identity")
|
||||||
|
|
||||||
|
sign_in_as @signup.identity
|
||||||
|
end
|
||||||
|
|
||||||
|
test "new" do
|
||||||
|
untenanted do
|
||||||
|
get saas.new_signup_completion_path
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "create" do
|
||||||
|
untenanted do
|
||||||
|
post saas.signup_completion_path, params: {
|
||||||
|
signup: {
|
||||||
|
full_name: @signup.full_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :redirect, "Valid params should redirect"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "create with invalid params" do
|
||||||
|
untenanted do
|
||||||
|
post saas.signup_completion_path, params: {
|
||||||
|
signup: {
|
||||||
|
full_name: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :unprocessable_entity, "Invalid params should return unprocessable entity"
|
||||||
|
end
|
||||||
|
end
|
||||||
Vendored
@@ -0,0 +1,61 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class Signup::AccountNameGeneratorTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
@identity = Identity.create!(email_address: "newart.userbaum@example.com")
|
||||||
|
@name = "Newart userbaum"
|
||||||
|
@generator = Signup::AccountNameGenerator.new(identity: @identity, name: @name)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "generate" do
|
||||||
|
account_name = @generator.generate
|
||||||
|
assert_equal "Newart's Fizzy", account_name, "The 1st account doesn't have 1st in the name"
|
||||||
|
|
||||||
|
first_account = Account.create!(external_account_id: "1st", name: account_name)
|
||||||
|
Current.without_account do
|
||||||
|
@identity.users.create!(account: first_account, name: @name)
|
||||||
|
@identity.reload
|
||||||
|
end
|
||||||
|
|
||||||
|
account_name = @generator.generate
|
||||||
|
assert_equal "Newart's 2nd Fizzy", account_name
|
||||||
|
|
||||||
|
second_account = Account.create!(external_account_id: "2nd", name: account_name)
|
||||||
|
Current.without_account do
|
||||||
|
@identity.users.create!(account: second_account, name: @name)
|
||||||
|
@identity.reload
|
||||||
|
end
|
||||||
|
|
||||||
|
account_name = @generator.generate
|
||||||
|
assert_equal "Newart's 3rd Fizzy", account_name
|
||||||
|
|
||||||
|
third_account = Account.create!(external_account_id: "3rd", name: account_name)
|
||||||
|
Current.without_account do
|
||||||
|
@identity.users.create!(account: third_account, name: @name)
|
||||||
|
@identity.reload
|
||||||
|
end
|
||||||
|
|
||||||
|
account_name = @generator.generate
|
||||||
|
assert_equal "Newart's 4th Fizzy", account_name
|
||||||
|
|
||||||
|
fourth_account = Account.create!(external_account_id: "4th", name: account_name)
|
||||||
|
Current.without_account do
|
||||||
|
@identity.users.create!(account: fourth_account, name: @name)
|
||||||
|
@identity.reload
|
||||||
|
end
|
||||||
|
|
||||||
|
account_name = @generator.generate
|
||||||
|
assert_equal "Newart's 5th Fizzy", account_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "generate continues from the previous highest index" do
|
||||||
|
account = Account.create!(external_account_id: "12th", name: "Newart's 12th Fizzy")
|
||||||
|
Current.without_account do
|
||||||
|
@identity.users.create!(account: account, name: @name)
|
||||||
|
@identity.reload
|
||||||
|
end
|
||||||
|
|
||||||
|
account_name = @generator.generate
|
||||||
|
assert_equal "Newart's 13th Fizzy", account_name
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class SignupTest < ActiveSupport::TestCase
|
||||||
|
test "#create_identity" do
|
||||||
|
signup = Signup.new(email_address: "brian@example.com")
|
||||||
|
|
||||||
|
assert_difference -> { Identity.count }, 1 do
|
||||||
|
assert_difference -> { MagicLink.count }, 1 do
|
||||||
|
assert signup.create_identity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_empty signup.errors
|
||||||
|
assert signup.identity
|
||||||
|
assert signup.identity.persisted?
|
||||||
|
|
||||||
|
signup_existing = Signup.new(email_address: "brian@example.com")
|
||||||
|
|
||||||
|
assert_no_difference -> { Identity.count } do
|
||||||
|
assert_difference -> { MagicLink.count }, 1 do
|
||||||
|
assert signup_existing.create_identity, "Should send magic link for existing identity"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
signup_invalid = Signup.new(email_address: "")
|
||||||
|
assert_raises do
|
||||||
|
signup_invalid.create_identity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#complete" do
|
||||||
|
Account.any_instance.expects(:setup_customer_template).once
|
||||||
|
Current.without_account do
|
||||||
|
signup = Signup.new(
|
||||||
|
full_name: "Kevin",
|
||||||
|
identity: identities(:kevin)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert signup.complete
|
||||||
|
|
||||||
|
assert signup.account
|
||||||
|
assert signup.user
|
||||||
|
assert_equal "Kevin", signup.user.name
|
||||||
|
|
||||||
|
signup_invalid = Signup.new(
|
||||||
|
full_name: "",
|
||||||
|
identity: identities(:kevin)
|
||||||
|
)
|
||||||
|
assert_not signup_invalid.complete
|
||||||
|
assert_not_empty signup_invalid.errors[:full_name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
require "queenbee/testing/mocks"
|
||||||
|
|
||||||
|
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