Fix database issues and add APNS configuration

- Fix owner_id type to UUID in devices migration
- Fix NOT NULL crash in device registration
- Add APNS config and 1Password integration
- Add --apns flag to bin/dev for local development
- Clean up devices controller
This commit is contained in:
Fernando Olivares
2026-01-15 19:07:15 -06:00
committed by Rosa Gutierrez
parent 3c54cd84fc
commit 55336873b2
13 changed files with 112 additions and 26 deletions
+18
View File
@@ -2,13 +2,31 @@
PORT=3006
USE_TAILSCALE=0
USE_APNS=0
for arg in "$@"; do
case $arg in
--tailscale) USE_TAILSCALE=1 ;;
--apns) USE_APNS=1 ;;
esac
done
if [ "$USE_APNS" = "1" ]; then
if [ ! -f tmp/saas.txt ]; then
echo "Enabling SaaS mode for APNs..."
./bin/rails saas:enable
fi
echo "Loading APNs credentials from 1Password..."
if ! eval "$(BUNDLE_GEMFILE=Gemfile.saas bundle exec apns-dev)"; then
echo "Error: failed to load APNs credentials. Are you signed into 1Password?" >&2
exit 1
fi
if [ -z "$APNS_ENCRYPTION_KEY" ] || [ -z "$APNS_KEY_ID" ]; then
echo "Error: APNs credentials not set. Missing APNS_ENCRYPTION_KEY or APNS_KEY_ID." >&2
exit 1
fi
fi
if [ ! -f tmp/solid-queue.txt ]; then
export SOLID_QUEUE_IN_PUMA=false
fi
+7
View File
@@ -0,0 +1,7 @@
<% if Fizzy.saas? %>
<%= ERB.new(File.read(Rails.root.join("saas/config/push.yml"))).result %>
<% else %>
shared:
apple: {}
google: {}
<% end %>
@@ -5,7 +5,7 @@ class CreateActionPushNativeDevices < ActiveRecord::Migration[8.0]
t.string :name
t.string :platform, null: false
t.string :token, null: false
t.belongs_to :owner, polymorphic: true
t.belongs_to :owner, polymorphic: true, type: :uuid
t.timestamps
end
Generated
+13
View File
@@ -69,6 +69,19 @@ ActiveRecord::Schema[8.2].define(version: 2026_02_18_120000) do
t.index ["external_account_id"], name: "index_accounts_on_external_account_id", unique: true
end
create_table "action_push_native_devices", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "name"
t.uuid "owner_id"
t.string "owner_type"
t.string "platform", null: false
t.string "token", null: false
t.datetime "updated_at", null: false
t.string "uuid", null: false
t.index ["owner_type", "owner_id", "uuid"], name: "idx_on_owner_type_owner_id_uuid_a42e3920d5", unique: true
t.index ["owner_type", "owner_id"], name: "index_action_push_native_devices_on_owner"
end
create_table "action_text_rich_texts", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.uuid "account_id", null: false
t.text "body", size: :long
+5 -1
View File
@@ -1,4 +1,4 @@
SECRETS=$(kamal secrets fetch --adapter 1password --account 23QPQDKZC5BKBIIG7UGT5GR5RM --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Beta/RAILS_MASTER_KEY Beta/MYSQL_ALTER_PASSWORD Beta/MYSQL_ALTER_USER Beta/MYSQL_APP_PASSWORD Beta/MYSQL_APP_USER Beta/MYSQL_READONLY_PASSWORD Beta/MYSQL_READONLY_USER Beta/SECRET_KEY_BASE Beta/VAPID_PUBLIC_KEY Beta/VAPID_PRIVATE_KEY Beta/ACTIVE_STORAGE_ACCESS_KEY_ID Beta/ACTIVE_STORAGE_SECRET_ACCESS_KEY Beta/QUEENBEE_API_TOKEN Beta/SIGNAL_ID_SECRET Beta/SENTRY_DSN Beta/ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY Beta/ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY Beta/ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT Beta/STRIPE_MONTHLY_V1_PRICE_ID Beta/STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID Beta/STRIPE_SECRET_KEY Beta/STRIPE_WEBHOOK_SECRET)
SECRETS=$(kamal secrets fetch --adapter 1password --account 23QPQDKZC5BKBIIG7UGT5GR5RM --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Beta/RAILS_MASTER_KEY Beta/MYSQL_ALTER_PASSWORD Beta/MYSQL_ALTER_USER Beta/MYSQL_APP_PASSWORD Beta/MYSQL_APP_USER Beta/MYSQL_READONLY_PASSWORD Beta/MYSQL_READONLY_USER Beta/SECRET_KEY_BASE Beta/VAPID_PUBLIC_KEY Beta/VAPID_PRIVATE_KEY Beta/ACTIVE_STORAGE_ACCESS_KEY_ID Beta/ACTIVE_STORAGE_SECRET_ACCESS_KEY Beta/QUEENBEE_API_TOKEN Beta/SIGNAL_ID_SECRET Beta/SENTRY_DSN Beta/ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY Beta/ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY Beta/ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT Beta/STRIPE_MONTHLY_V1_PRICE_ID Beta/STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID Beta/STRIPE_SECRET_KEY Beta/STRIPE_WEBHOOK_SECRET Beta/APNS_ENCRYPTION_KEY Beta/APNS_KEY_ID Beta/APNS_TEAM_ID Beta/APNS_TOPIC)
GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
@@ -25,3 +25,7 @@ STRIPE_MONTHLY_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_V1_PRICE_ID $S
STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID $SECRETS)
STRIPE_SECRET_KEY=$(kamal secrets extract STRIPE_SECRET_KEY $SECRETS)
STRIPE_WEBHOOK_SECRET=$(kamal secrets extract STRIPE_WEBHOOK_SECRET $SECRETS)
APNS_ENCRYPTION_KEY=$(kamal secrets extract APNS_ENCRYPTION_KEY $SECRETS)
APNS_KEY_ID=$(kamal secrets extract APNS_KEY_ID $SECRETS)
APNS_TEAM_ID=$(kamal secrets extract APNS_TEAM_ID $SECRETS)
APNS_TOPIC=$(kamal secrets extract APNS_TOPIC $SECRETS)
+5 -1
View File
@@ -1,4 +1,4 @@
SECRETS=$(kamal secrets fetch --adapter 1password --account 23QPQDKZC5BKBIIG7UGT5GR5RM --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Production/RAILS_MASTER_KEY Production/MYSQL_ALTER_PASSWORD Production/MYSQL_ALTER_USER Production/MYSQL_APP_PASSWORD Production/MYSQL_APP_USER Production/MYSQL_READONLY_PASSWORD Production/MYSQL_READONLY_USER Production/SECRET_KEY_BASE Production/VAPID_PUBLIC_KEY Production/VAPID_PRIVATE_KEY Production/ACTIVE_STORAGE_ACCESS_KEY_ID Production/ACTIVE_STORAGE_SECRET_ACCESS_KEY Production/QUEENBEE_API_TOKEN Production/SIGNAL_ID_SECRET Production/SENTRY_DSN Production/ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY Production/ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY Production/ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT Production/STRIPE_MONTHLY_V1_PRICE_ID Production/STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID Production/STRIPE_SECRET_KEY Production/STRIPE_WEBHOOK_SECRET)
SECRETS=$(kamal secrets fetch --adapter 1password --account 23QPQDKZC5BKBIIG7UGT5GR5RM --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Production/RAILS_MASTER_KEY Production/MYSQL_ALTER_PASSWORD Production/MYSQL_ALTER_USER Production/MYSQL_APP_PASSWORD Production/MYSQL_APP_USER Production/MYSQL_READONLY_PASSWORD Production/MYSQL_READONLY_USER Production/SECRET_KEY_BASE Production/VAPID_PUBLIC_KEY Production/VAPID_PRIVATE_KEY Production/ACTIVE_STORAGE_ACCESS_KEY_ID Production/ACTIVE_STORAGE_SECRET_ACCESS_KEY Production/QUEENBEE_API_TOKEN Production/SIGNAL_ID_SECRET Production/SENTRY_DSN Production/ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY Production/ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY Production/ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT Production/STRIPE_MONTHLY_V1_PRICE_ID Production/STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID Production/STRIPE_SECRET_KEY Production/STRIPE_WEBHOOK_SECRET Production/APNS_ENCRYPTION_KEY Production/APNS_KEY_ID Production/APNS_TEAM_ID Production/APNS_TOPIC)
GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
@@ -25,3 +25,7 @@ STRIPE_MONTHLY_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_V1_PRICE_ID $S
STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID $SECRETS)
STRIPE_SECRET_KEY=$(kamal secrets extract STRIPE_SECRET_KEY $SECRETS)
STRIPE_WEBHOOK_SECRET=$(kamal secrets extract STRIPE_WEBHOOK_SECRET $SECRETS)
APNS_ENCRYPTION_KEY=$(kamal secrets extract APNS_ENCRYPTION_KEY $SECRETS)
APNS_KEY_ID=$(kamal secrets extract APNS_KEY_ID $SECRETS)
APNS_TEAM_ID=$(kamal secrets extract APNS_TEAM_ID $SECRETS)
APNS_TOPIC=$(kamal secrets extract APNS_TOPIC $SECRETS)
+5 -1
View File
@@ -1,4 +1,4 @@
SECRETS=$(kamal secrets fetch --adapter 1password --account 23QPQDKZC5BKBIIG7UGT5GR5RM --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Staging/RAILS_MASTER_KEY Staging/MYSQL_ALTER_PASSWORD Staging/MYSQL_ALTER_USER Staging/MYSQL_APP_PASSWORD Staging/MYSQL_APP_USER Staging/MYSQL_READONLY_PASSWORD Staging/MYSQL_READONLY_USER Staging/SECRET_KEY_BASE Staging/VAPID_PUBLIC_KEY Staging/VAPID_PRIVATE_KEY Staging/ACTIVE_STORAGE_ACCESS_KEY_ID Staging/ACTIVE_STORAGE_SECRET_ACCESS_KEY Staging/QUEENBEE_API_TOKEN Staging/SIGNAL_ID_SECRET Staging/SENTRY_DSN Staging/ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY Staging/ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY Staging/ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT Staging/STRIPE_MONTHLY_V1_PRICE_ID Staging/STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID Staging/STRIPE_SECRET_KEY Staging/STRIPE_WEBHOOK_SECRET)
SECRETS=$(kamal secrets fetch --adapter 1password --account 23QPQDKZC5BKBIIG7UGT5GR5RM --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Staging/RAILS_MASTER_KEY Staging/MYSQL_ALTER_PASSWORD Staging/MYSQL_ALTER_USER Staging/MYSQL_APP_PASSWORD Staging/MYSQL_APP_USER Staging/MYSQL_READONLY_PASSWORD Staging/MYSQL_READONLY_USER Staging/SECRET_KEY_BASE Staging/VAPID_PUBLIC_KEY Staging/VAPID_PRIVATE_KEY Staging/ACTIVE_STORAGE_ACCESS_KEY_ID Staging/ACTIVE_STORAGE_SECRET_ACCESS_KEY Staging/QUEENBEE_API_TOKEN Staging/SIGNAL_ID_SECRET Staging/SENTRY_DSN Staging/ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY Staging/ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY Staging/ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT Staging/STRIPE_MONTHLY_V1_PRICE_ID Staging/STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID Staging/STRIPE_SECRET_KEY Staging/STRIPE_WEBHOOK_SECRET Staging/APNS_ENCRYPTION_KEY Staging/APNS_KEY_ID Staging/APNS_TEAM_ID Staging/APNS_TOPIC)
GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
@@ -25,3 +25,7 @@ STRIPE_MONTHLY_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_V1_PRICE_ID $S
STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID $SECRETS)
STRIPE_SECRET_KEY=$(kamal secrets extract STRIPE_SECRET_KEY $SECRETS)
STRIPE_WEBHOOK_SECRET=$(kamal secrets extract STRIPE_WEBHOOK_SECRET $SECRETS)
APNS_ENCRYPTION_KEY=$(kamal secrets extract APNS_ENCRYPTION_KEY $SECRETS)
APNS_KEY_ID=$(kamal secrets extract APNS_KEY_ID $SECRETS)
APNS_TEAM_ID=$(kamal secrets extract APNS_TEAM_ID $SECRETS)
APNS_TOPIC=$(kamal secrets extract APNS_TOPIC $SECRETS)
@@ -1,29 +1,30 @@
class Users::DevicesController < ApplicationController
before_action :set_devices
def index
@devices = Current.user.devices.order(created_at: :desc)
end
def create
attrs = device_params
device = Current.user.devices.find_or_create_by(uuid: attrs[:uuid])
device.update!(token: attrs[:token], name: attrs[:name], platform: attrs[:platform])
device = @devices.find_or_initialize_by(uuid: params.require(:uuid))
device.update!(device_params)
head :created
rescue ActiveRecord::RecordInvalid
head :unprocessable_entity
rescue ArgumentError
head :bad_request
end
def destroy
Current.user.devices.find_by(id: params[:id])&.destroy
@devices.destroy_by(id: params[:id])
redirect_to users_devices_path, notice: "Device removed"
end
private
def set_devices
@devices = Current.user.devices.order(created_at: :desc)
end
def device_params
params.permit(:uuid, :token, :platform, :name).tap do |p|
p[:platform] = p[:platform].to_s.downcase
raise ActionController::BadRequest unless p[:platform].in?(%w[apple google])
raise ActionController::BadRequest if p[:uuid].blank?
raise ActionController::BadRequest if p[:token].blank?
params.permit(:token, :platform, :name).tap do |permitted|
permitted[:platform] = permitted[:platform].to_s.downcase if permitted[:platform].present?
end
end
end
@@ -1,4 +1,4 @@
class ApplicationPushNotification < ActionPushNative::Notification
queue_as :default
self.enabled = !Rails.env.local?
self.enabled = Fizzy.saas? && (!Rails.env.local? || ENV["ENABLE_NATIVE_PUSH"] == "true")
end
+4 -5
View File
@@ -1,11 +1,10 @@
shared:
apple:
key_id: <%= ENV["APNS_KEY_ID"] || Rails.application.credentials.dig(:action_push_native, :apns, :key_id) %>
encryption_key: <%= (ENV["APNS_ENCRYPTION_KEY"] || Rails.application.credentials.dig(:action_push_native, :apns, :key))&.dump %>
team_id: YOUR_TEAM_ID # Your 10-character Apple Developer Team ID (not secret)
topic: com.yourcompany.fizzy # Your app's bundle identifier (not secret)
# Uncomment for local development with Xcode builds (uses APNs sandbox):
# connect_to_development_server: true
encryption_key: <%= (ENV["APNS_ENCRYPTION_KEY"]&.gsub("\\n", "\n") || Rails.application.credentials.dig(:action_push_native, :apns, :key))&.dump %>
team_id: <%= ENV["APNS_TEAM_ID"] || Rails.application.credentials.dig(:action_push_native, :apns, :team_id) || "2WNYUYRS7G" %>
topic: <%= ENV["APNS_TOPIC"] || Rails.application.credentials.dig(:action_push_native, :apns, :topic) || "do.fizzy.app.ios" %>
connect_to_development_server: <%= Rails.env.local? %>
google:
encryption_key: <%= (ENV["FCM_ENCRYPTION_KEY"] || Rails.application.credentials.dig(:action_push_native, :fcm, :key))&.dump %>
project_id: your-firebase-project # Your Firebase project ID (not secret)
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env ruby
#
# Fetches APNs development environment variables from 1Password.
#
# Usage: eval "$(bundle exec apns-dev)"
OP_ACCOUNT = "23QPQDKZC5BKBIIG7UGT5GR5RM"
OP_VAULT = "Mobile"
OP_ITEM = "37signals Push Notifications key"
def op_read(field)
`op read "op://#{OP_VAULT}/#{OP_ITEM}/#{field}" --account #{OP_ACCOUNT} 2>/dev/null`.strip
end
key_id = op_read("key ID")
team_id = op_read("team ID")
encryption_key = op_read("AuthKey_3CR5J2W8Q6.p8")
if key_id.empty? || encryption_key.empty?
warn "Error: Could not fetch APNs credentials from 1Password"
warn "Make sure you're signed in: op signin --account #{OP_ACCOUNT}"
exit 1
end
puts %Q(export APNS_KEY_ID="#{key_id}")
puts %Q(export APNS_TEAM_ID="#{team_id}")
puts %Q(export APNS_ENCRYPTION_KEY="#{encryption_key.gsub("\n", "\\n")}")
puts %Q(export APNS_TOPIC="do.fizzy.app.ios")
puts %Q(export ENABLE_NATIVE_PUSH="true")
warn ""
warn "APNs credentials loaded for development"
warn " Key ID: #{key_id}"
warn " Team ID: #{team_id}"
warn " Topic: do.fizzy.app.ios"
warn " Native push: enabled"
+1 -1
View File
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
end
spec.bindir = "exe"
spec.executables = [ "stripe-dev" ]
spec.executables = [ "apns-dev", "stripe-dev" ]
spec.add_dependency "rails", ">= 8.1.0.beta1"
spec.add_dependency "queenbee"
+3 -3
View File
@@ -23,8 +23,8 @@ module Fizzy
headers: app.config.public_file_server.headers
end
initializer "fizzy_saas.push_config", before: "action_push_native.config" do |app|
app.paths.add "config/push", with: root.join("config/push.yml")
initializer "fizzy_saas.push_config", after: "action_push_native.config" do |app|
app.paths["config/push"].unshift(root.join("config/push.yml").to_s)
end
initializer "fizzy.saas.routes", after: :add_routing_paths do |app|
@@ -157,7 +157,7 @@ module Fizzy
::Account.include Account::Billing, Account::Limited
::User.include User::NotifiesAccountOfEmailChange
::User.include User::Devices
::NotificationPusher.include NotificationPusher::Native
::NotificationPusher.prepend NotificationPusher::Native
::Signup.prepend Fizzy::Saas::Signup
CardsController.include(Card::LimitedCreation)
Cards::PublishesController.include(Card::LimitedPublishing)