diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb
index 5ffd86551..8d30d0cbd 100644
--- a/app/controllers/concerns/authentication.rb
+++ b/app/controllers/concerns/authentication.rb
@@ -21,6 +21,7 @@ module Authentication
def allow_unauthenticated_access(**options)
skip_before_action :require_authentication, **options
+ before_action :resume_identity, **options
before_action :resume_session, **options
end
@@ -30,9 +31,14 @@ module Authentication
before_action :redirect_tenanted_request, **options
end
- def require_identity(**options)
+ def require_identified_access(**options)
before_action :require_identity, **options
end
+
+ def require_unidentified_access(**options)
+ before_action :resume_identity, **options
+ before_action :redirect_request_with_identification, **options
+ end
end
private
@@ -110,6 +116,10 @@ module Authentication
redirect_to root_url if ApplicationRecord.current_tenant
end
+ def redirect_request_with_identification
+ redirect_to session_login_menu_path(script_name: nil) if Current.identity_token.present?
+ end
+
def start_new_session_for(user)
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
set_current_session session
diff --git a/app/controllers/sessions/login_menus_controller.rb b/app/controllers/sessions/login_menus_controller.rb
index 03c5a4083..ebd06191e 100644
--- a/app/controllers/sessions/login_menus_controller.rb
+++ b/app/controllers/sessions/login_menus_controller.rb
@@ -1,5 +1,6 @@
class Sessions::LoginMenusController < ApplicationController
require_untenanted_access
+ require_identified_access
def show
@tenants = IdentityProvider.tenants_for(resume_identity)
diff --git a/app/controllers/sessions/magic_links_controller.rb b/app/controllers/sessions/magic_links_controller.rb
index 51426e43c..a955cda75 100644
--- a/app/controllers/sessions/magic_links_controller.rb
+++ b/app/controllers/sessions/magic_links_controller.rb
@@ -1,5 +1,6 @@
class Sessions::MagicLinksController < ApplicationController
require_untenanted_access
+ require_unidentified_access
rate_limit to: 10, within: 15.minutes, only: :create, with: -> { redirect_to session_magic_link_path, alert: "Try again in 15 minutes." }
def show
diff --git a/app/controllers/sessions/starts_controller.rb b/app/controllers/sessions/starts_controller.rb
index 8661634f3..f0db9d89a 100644
--- a/app/controllers/sessions/starts_controller.rb
+++ b/app/controllers/sessions/starts_controller.rb
@@ -1,5 +1,6 @@
class Sessions::StartsController < ApplicationController
allow_unauthenticated_access
+ require_identified_access
def new
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 139e60dcd..ef54fc34f 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -1,5 +1,6 @@
class SessionsController < ApplicationController
require_untenanted_access only: %i[ new create ]
+ require_unidentified_access only: %i[ new create ]
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
def new
diff --git a/app/controllers/users/email_addresses_controller.rb b/app/controllers/users/email_addresses_controller.rb
index 0595e3470..c9c370315 100644
--- a/app/controllers/users/email_addresses_controller.rb
+++ b/app/controllers/users/email_addresses_controller.rb
@@ -6,8 +6,12 @@ class Users::EmailAddressesController < ApplicationController
end
def create
- token = @user.generate_email_address_change_token(to: new_email_address, expires_in: 30.minutes)
- UserMailer.email_change_confirmation(user: @user, email_address: new_email_address, token: token).deliver_later
+ if User.exists?(email_address: new_email_address)
+ flash.now[:alert] = "Someone else already uses that email"
+ render :new, status: :unprocessable_entity
+ else
+ @user.send_email_address_change_confirmation(new_email_address)
+ end
end
private
diff --git a/app/models/account/join_code.rb b/app/models/account/join_code.rb
index c536f7c6c..2ed42e5c1 100644
--- a/app/models/account/join_code.rb
+++ b/app/models/account/join_code.rb
@@ -11,10 +11,13 @@ class Account::JoinCode < ApplicationRecord
class << self
def redeem(code)
- find_by(code: code)&.tap do |join_code|
- if join_code.active?
- join_code.increment!(:usage_count)
- end
+ join_code = find_by(code: code)
+
+ if join_code&.active?
+ join_code.increment!(:usage_count)
+ true
+ else
+ false
end
end
@@ -28,7 +31,9 @@ class Account::JoinCode < ApplicationRecord
end
def reset
- update!(code: generate_code, usage_count: 0)
+ generate_code
+ self.usage_count = 0
+ save!
end
private
diff --git a/app/models/identity_provider.rb b/app/models/identity_provider.rb
index c76872487..2fb22bfd2 100644
--- a/app/models/identity_provider.rb
+++ b/app/models/identity_provider.rb
@@ -1,4 +1,6 @@
module IdentityProvider
+ class Error < StandardError; end
+
Token = Data.define(:id, :updated_at) do
delegate :dig, to: :to_h
@@ -11,7 +13,7 @@ module IdentityProvider
extend self
- mattr_accessor :backend, default: IdentityProvider::Simple
+ mattr_accessor :backend, default: IdentityProvider::LocalBackend
delegate :link, :unlink, :change_email_address, :send_magic_link, :consume_magic_link, :tenants_for, :token_for, :resolve_token, :verify_token, to: :backend
end
diff --git a/app/models/identity_provider/simple.rb b/app/models/identity_provider/local_backend.rb
similarity index 97%
rename from app/models/identity_provider/simple.rb
rename to app/models/identity_provider/local_backend.rb
index 122a0b431..99b709110 100644
--- a/app/models/identity_provider/simple.rb
+++ b/app/models/identity_provider/local_backend.rb
@@ -1,4 +1,4 @@
-module IdentityProvider::Simple
+module IdentityProvider::LocalBackend
extend self
def link(email_address:, to:)
diff --git a/app/models/user.rb b/app/models/user.rb
index cc4c3965c..d18450332 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -20,15 +20,20 @@ class User < ApplicationRecord
normalizes :email_address, with: ->(value) { value.strip.downcase }
def deactivate
+ old_email_address = email_address
+
sessions.delete_all
accesses.destroy_all
- old_email = email_address
- update! active: false, email_address: deactived_email_address
- unlink_identity
+
+ update! active: false, email_address: deactivated_email_address
+ IdentityProvider.unlink(email_address: old_email_address, from: tenant)
+ rescue => e
+ update! active: true, email_address: old_email_address
+ raise e
end
private
- def deactived_email_address
+ def deactivated_email_address
email_address.sub(/@/, "-deactivated-#{SecureRandom.uuid}@")
end
end
diff --git a/app/models/user/email_address_changeable.rb b/app/models/user/email_address_changeable.rb
index da2137b39..784239079 100644
--- a/app/models/user/email_address_changeable.rb
+++ b/app/models/user/email_address_changeable.rb
@@ -1,8 +1,14 @@
module User::EmailAddressChangeable
EMAIL_CHANGE_TOKEN_PURPOSE = "change_email_address"
+ EMAIL_CHANGE_TOKEN_EXPIRATION = 30.minutes
extend ActiveSupport::Concern
+ def send_email_address_change_confirmation(new_email_address)
+ token = generate_email_address_change_token(to: new_email_address, expires_in: EMAIL_CHANGE_TOKEN_EXPIRATION)
+ UserMailer.email_change_confirmation(user: self, email_address: new_email_address, token: token).deliver_later
+ end
+
def generate_email_address_change_token(from: email_address, to:, **options)
options = options.reverse_merge(
for: EMAIL_CHANGE_TOKEN_PURPOSE,
@@ -23,7 +29,20 @@ module User::EmailAddressChangeable
elsif email_address != parsed_token.params.fetch("old_email_address")
raise ArgumentError, "The token was generated for a different email address"
else
- update!(email_address: parsed_token.params.fetch("new_email_address"))
+ change_email_address(parsed_token.params.fetch("new_email_address"))
end
end
+
+ private
+ def change_email_address(new_email_address)
+ old_email_address = email_address
+ update!(email_address: new_email_address)
+
+ begin
+ IdentityProvider.change_email_address(from: old_email_address, to: new_email_address, tenant: tenant)
+ rescue => e
+ update!(email_address: old_email_address)
+ raise e
+ end
+ end
end
diff --git a/app/models/user/identifiable.rb b/app/models/user/identifiable.rb
index 58cac51ea..abe7b412a 100644
--- a/app/models/user/identifiable.rb
+++ b/app/models/user/identifiable.rb
@@ -3,7 +3,6 @@ module User::Identifiable
included do
after_create_commit :link_identity, unless: :system?
- after_update_commit :update_email_address_on_identity, if: -> { saved_change_to_email_address? && !system? }
after_destroy_commit :unlink_identity, unless: :system?
end
@@ -19,9 +18,4 @@ module User::Identifiable
def unlink_identity
IdentityProvider.unlink(email_address: email_address, from: tenant)
end
-
- def update_email_address_on_identity
- old_email, new_email = saved_change_to_email_address
- IdentityProvider.change_email_address(from: old_email, to: new_email, tenant: tenant)
- end
end
diff --git a/app/views/sessions/starts/new.html.erb b/app/views/sessions/starts/new.html.erb
index 904eb5432..1438d5271 100644
--- a/app/views/sessions/starts/new.html.erb
+++ b/app/views/sessions/starts/new.html.erb
@@ -1,5 +1,5 @@
<% @hide_footer_frames = true %>
-<% @page_title = "Signing into #{Account.sole.name}" %>
+<% @page_title = "Signing in..." %>
<% content_for :header do %>
<% end %>
-
-
<%= @page_title %>
+
+
<%= form_with url: session_start_path, method: :post, data: { controller: "auto-submit" } do |form| %>
- <%= form.button "Continue", type: "submit", class: "btn btn-primary" %>
+ <%= form.button "Sign in", type: "submit", class: "btn btn-primary", data: { turbo_submits_with: "Signing in..." } %>
<% end %>
diff --git a/db/schema.rb b/db/schema.rb
index 86470ce07..e4c3b3491 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -24,6 +24,15 @@ ActiveRecord::Schema[8.2].define(version: 2025_10_29_161222) do
t.index ["user_id"], name: "index_accesses_on_user_id"
end
+ create_table "account_join_codes", force: :cascade do |t|
+ t.string "code", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.integer "usage_count", default: 0, null: false
+ t.integer "usage_limit", default: 10, null: false
+ t.index ["code"], name: "index_account_join_codes_on_code", unique: true
+ end
+
create_table "accounts", force: :cascade do |t|
t.datetime "created_at", null: false
t.integer "external_account_id"
diff --git a/db/schema_cache.yml b/db/schema_cache.yml
index 4ca90d0bd..ff60d4302 100644
--- a/db/schema_cache.yml
+++ b/db/schema_cache.yml
@@ -20,7 +20,7 @@ columns:
default_function:
collation:
comment:
- - &23 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &24 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: collection_id
cast_type: &3 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer
@@ -40,7 +40,7 @@ columns:
default_function:
collation:
comment:
- - &5 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &7 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: created_at
cast_type: *1
@@ -50,7 +50,7 @@ columns:
default_function:
collation:
comment:
- - &6 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &8 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment: true
name: id
cast_type: *3
@@ -63,13 +63,13 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: involvement
- cast_type: &7 !ruby/object:ActiveModel::Type::String
+ cast_type: &5 !ruby/object:ActiveModel::Type::String
true: t
false: f
precision:
scale:
limit:
- sql_type_metadata: &8 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type_metadata: &6 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
sql_type: varchar
type: :string
limit:
@@ -90,7 +90,7 @@ columns:
default_function:
collation:
comment:
- - &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &18 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: user_id
cast_type: *3
@@ -100,8 +100,42 @@ columns:
default_function:
collation:
comment:
+ account_join_codes:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: code
+ cast_type: *5
+ sql_type_metadata: *6
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ - *7
+ - *8
+ - *9
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: usage_count
+ cast_type: *3
+ sql_type_metadata: *4
+ 'null': false
+ default: 0
+ default_function:
+ collation:
+ comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: usage_limit
+ cast_type: *3
+ sql_type_metadata: *4
+ 'null': false
+ default: 10
+ default_function:
+ collation:
+ comment:
accounts:
- - *5
+ - *7
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: external_account_id
@@ -112,22 +146,12 @@ columns:
default_function:
collation:
comment:
- - *6
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: join_code
- cast_type: *7
- sql_type_metadata: *8
- 'null': true
- default:
- default_function:
- collation:
- comment:
+ - *8
- &10 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: name
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': false
default:
default_function:
@@ -155,8 +179,8 @@ columns:
default_function:
collation:
comment:
- - *5
- - *6
+ - *7
+ - *8
- *10
- &13 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
@@ -181,8 +205,8 @@ columns:
- &14 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: record_type
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': false
default:
default_function:
@@ -200,16 +224,16 @@ columns:
default_function:
collation:
comment:
- - *5
- - *6
+ - *7
+ - *8
- *10
- *13
- *14
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: slug
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': true
default:
default_function:
@@ -229,8 +253,8 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: checksum
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': true
default:
default_function:
@@ -239,30 +263,30 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: content_type
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': true
default:
default_function:
collation:
comment:
- - *5
+ - *7
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: filename
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': false
default:
default_function:
collation:
comment:
- - *6
- - &18 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - *8
+ - &19 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: key
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': false
default:
default_function:
@@ -281,8 +305,8 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: service_name
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': false
default:
default_function:
@@ -290,33 +314,68 @@ columns:
comment:
active_storage_variant_records:
- *17
- - *6
+ - *8
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: variation_digest
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': false
default:
default_function:
collation:
comment:
- ar_internal_metadata:
- - *5
+ ai_quotas:
+ - *7
+ - *8
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: limit
+ cast_type: *3
+ sql_type_metadata: *4
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: reset_at
+ cast_type: *1
+ sql_type_metadata: *2
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ - *9
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: used
+ cast_type: *3
+ sql_type_metadata: *4
+ 'null': false
+ default: 0
+ default_function:
+ collation:
+ comment:
- *18
+ ar_internal_metadata:
+ - *7
+ - *19
- *9
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: value
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': true
default:
default_function:
collation:
comment:
assignees_filters:
- - &20 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &21 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: assignee_id
cast_type: *3
@@ -326,7 +385,7 @@ columns:
default_function:
collation:
comment:
- - &19 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &20 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: filter_id
cast_type: *3
@@ -337,7 +396,7 @@ columns:
collation:
comment:
assigners_filters:
- - &21 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &22 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: assigner_id
cast_type: *3
@@ -347,11 +406,11 @@ columns:
default_function:
collation:
comment:
- - *19
- assignments:
- *20
+ assignments:
- *21
- - &22 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - *22
+ - &23 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: card_id
cast_type: *3
@@ -361,13 +420,13 @@ columns:
default_function:
collation:
comment:
- - *5
- - *6
+ - *7
+ - *8
- *9
card_activity_spikes:
- - *22
- - *5
- - *6
+ - *23
+ - *7
+ - *8
- *9
card_engagements:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
@@ -380,13 +439,13 @@ columns:
default_function:
collation:
comment:
- - *5
- - *6
+ - *7
+ - *8
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: status
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': false
default: doing
default_function:
@@ -394,16 +453,16 @@ columns:
comment:
- *9
card_goldnesses:
- - *22
- - *5
- - *6
+ - *23
+ - *7
+ - *8
- *9
card_not_nows:
- - *22
- - *5
- - *6
+ - *23
+ - *7
+ - *8
- *9
- - &24 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &25 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: user_id
cast_type: *3
@@ -414,7 +473,7 @@ columns:
collation:
comment:
cards:
- - *23
+ - *24
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: column_id
@@ -425,8 +484,8 @@ columns:
default_function:
collation:
comment:
- - *5
- - &25 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - *7
+ - &26 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: creator_id
cast_type: *3
@@ -439,12 +498,12 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: due_on
- cast_type: !ruby/object:ActiveRecord::Type::Date
+ cast_type: &38 !ruby/object:ActiveRecord::Type::Date
precision:
scale:
limit:
timezone:
- sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type_metadata: &39 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
sql_type: date
type: :date
limit:
@@ -455,7 +514,7 @@ columns:
default_function:
collation:
comment:
- - *6
+ - *8
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: last_active_at
@@ -476,11 +535,11 @@ columns:
default_function:
collation:
comment:
- - &35 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &37 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: title
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': true
default:
default_function:
@@ -498,22 +557,22 @@ columns:
default_function:
collation:
comment:
- - *19
+ - *20
closures:
- - *22
- - *5
- - *6
- - *9
- - *24
- collection_publications:
- *23
- - *5
- - *6
+ - *7
+ - *8
+ - *9
+ - *25
+ collection_publications:
+ - *24
+ - *7
+ - *8
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: key
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': true
default:
default_function:
@@ -524,11 +583,11 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: all_access
- cast_type: &32 !ruby/object:ActiveModel::Type::Boolean
+ cast_type: &33 !ruby/object:ActiveModel::Type::Boolean
precision:
scale:
limit:
- sql_type_metadata: &33 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type_metadata: &34 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
sql_type: boolean
type: :boolean
limit:
@@ -539,49 +598,158 @@ columns:
default_function:
collation:
comment:
- - *5
- - *25
- - *6
+ - *7
+ - *26
+ - *8
- *10
- *9
collections_filters:
- - *23
- - *19
+ - *24
+ - *20
columns:
- - *23
+ - *24
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: color
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': false
default:
default_function:
collation:
comment:
- - *5
- - *6
+ - *7
+ - *8
- *10
+ - *9
+ comments:
+ - *23
+ - *7
+ - *26
+ - *8
+ - *9
+ conversation_messages:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
- name: position
+ name: client_message_id
+ cast_type: *5
+ sql_type_metadata: *6
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: conversation_id
cast_type: *3
sql_type_metadata: *4
'null': false
- default: 0
+ default:
+ default_function:
+ collation:
+ comment:
+ - &31 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: cost_in_microcents
+ cast_type: *11
+ sql_type_metadata: *12
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
+ - *7
+ - *8
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: input_cost_in_microcents
+ cast_type: *11
+ sql_type_metadata: *12
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: input_tokens
+ cast_type: *11
+ sql_type_metadata: *12
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: model_id
+ cast_type: *5
+ sql_type_metadata: *6
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: output_cost_in_microcents
+ cast_type: *11
+ sql_type_metadata: *12
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: output_tokens
+ cast_type: *11
+ sql_type_metadata: *12
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: role
+ cast_type: *5
+ sql_type_metadata: *6
+ 'null': false
+ default:
default_function:
collation:
comment:
- *9
- comments:
- - *22
- - *5
- - *25
- - *6
+ conversations:
+ - *7
+ - *8
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: state
+ cast_type: *5
+ sql_type_metadata: *6
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: string
+ cast_type: *5
+ sql_type_metadata: *6
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
- *9
+ - *18
creators_filters:
- - *25
- - *19
+ - *26
+ - *20
entropy_configurations:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
@@ -606,30 +774,30 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: container_type
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': false
default:
default_function:
collation:
comment:
- - *5
- - *6
+ - *7
+ - *8
- *9
events:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: action
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': false
default:
default_function:
collation:
comment:
- - *23
- - *5
- - *25
+ - *24
+ - *7
+ - *26
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: eventable_id
@@ -643,22 +811,22 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: eventable_type
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': false
default:
default_function:
collation:
comment:
- - *6
+ - *8
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: particulars
- cast_type: &26 !ruby/object:ActiveRecord::Type::Json
+ cast_type: &27 !ruby/object:ActiveRecord::Type::Json
precision:
scale:
limit:
- sql_type_metadata: &27 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type_metadata: &28 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
sql_type: json
type: :json
limit:
@@ -671,24 +839,24 @@ columns:
comment:
- *9
filters:
- - *5
- - *25
+ - *7
+ - *26
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: fields
- cast_type: *26
- sql_type_metadata: *27
+ cast_type: *27
+ sql_type_metadata: *28
'null': false
default: "{}"
default_function:
collation:
comment:
- - *6
+ - *8
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: params_digest
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': false
default:
default_function:
@@ -696,8 +864,8 @@ columns:
comment:
- *9
filters_tags:
- - *19
- - &34 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - *20
+ - &36 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: tag_id
cast_type: *3
@@ -708,8 +876,8 @@ columns:
collation:
comment:
mentions:
- - *5
- - *6
+ - *7
+ - *8
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: mentionee_id
@@ -743,8 +911,8 @@ columns:
- &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: source_type
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': false
default:
default_function:
@@ -752,7 +920,7 @@ columns:
comment:
- *9
notification_bundles:
- - *5
+ - *7
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: ends_at
@@ -763,7 +931,7 @@ columns:
default_function:
collation:
comment:
- - *6
+ - *8
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: starts_at
@@ -785,9 +953,9 @@ columns:
collation:
comment:
- *9
- - *28
+ - *18
notifications:
- - *5
+ - *7
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: creator_id
@@ -798,7 +966,7 @@ columns:
default_function:
collation:
comment:
- - *6
+ - *8
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: read_at
@@ -812,58 +980,74 @@ columns:
- *29
- *30
- *9
- - *28
- pins:
- - *22
- - *5
- - *6
+ - *18
+ period_highlights:
+ - &35 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: content
+ cast_type: *15
+ sql_type_metadata: *16
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ - *31
+ - *7
+ - *8
+ - *19
- *9
- - *28
+ pins:
+ - *23
+ - *7
+ - *8
+ - *9
+ - *18
push_subscriptions:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: auth_key
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': true
default:
default_function:
collation:
comment:
- - *5
+ - *7
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: endpoint
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': true
default:
default_function:
collation:
comment:
- - *6
+ - *8
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: p256dh_key
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': true
default:
default_function:
collation:
comment:
- *9
- - &31 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &32 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: user_agent
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': true
default:
default_function:
collation:
comment:
- - *28
+ - *18
reactions:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
@@ -895,8 +1079,8 @@ columns:
default_function:
collation:
comment:
- - *5
- - *6
+ - *7
+ - *8
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: reacter_id
@@ -912,16 +1096,53 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: version
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ search_embeddings_vector_chunks00:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: rowid
+ cast_type: !ruby/object:ActiveModel::Type::Value
+ precision:
+ scale:
+ limit:
+ sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type: ''
+ type:
+ limit:
+ precision:
+ scale:
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: vectors
+ cast_type: !ruby/object:ActiveModel::Type::Binary
+ precision:
+ scale:
+ limit:
+ sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type: BLOB
+ type: :binary
+ limit:
+ precision:
+ scale:
'null': false
default:
default_function:
collation:
comment:
search_queries:
- - *5
- - *6
+ - *7
+ - *8
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: terms
@@ -943,62 +1164,53 @@ columns:
collation:
comment:
- *9
- - *28
+ - *18
search_results:
- - *5
- - *6
+ - *7
+ - *8
- *9
sessions:
- - *5
- - *6
+ - *7
+ - *8
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: ip_address
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': true
default:
default_function:
collation:
comment:
- *9
- - *31
- - *28
+ - *32
+ - *18
steps:
- - *22
+ - *23
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: completed
- cast_type: *32
- sql_type_metadata: *33
+ cast_type: *33
+ sql_type_metadata: *34
'null': false
default: false
default_function:
collation:
comment:
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: content
- cast_type: *15
- sql_type_metadata: *16
- 'null': false
- default:
- default_function:
- collation:
- comment:
- - *5
- - *6
+ - *35
+ - *7
+ - *8
- *9
taggings:
- - *22
- - *5
- - *6
- - *34
+ - *23
+ - *7
+ - *8
+ - *36
- *9
tags:
- - *5
- - *6
- - *35
+ - *7
+ - *8
+ - *37
- *9
user_settings:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
@@ -1011,49 +1223,74 @@ columns:
default_function:
collation:
comment:
- - *5
- - *6
+ - *7
+ - *8
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: timezone_name
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': true
default:
default_function:
collation:
comment:
- *9
- - *28
+ - *18
+ user_weekly_highlights:
+ - *7
+ - *8
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: period_highlights_id
+ cast_type: *3
+ sql_type_metadata: *4
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: starts_at
+ cast_type: *38
+ sql_type_metadata: *39
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ - *9
+ - *18
users:
- - &37 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &41 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: active
- cast_type: *32
- sql_type_metadata: *33
+ cast_type: *33
+ sql_type_metadata: *34
'null': false
default: true
default_function:
collation:
comment:
- - *5
+ - *7
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: email_address
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': true
default:
default_function:
collation:
comment:
- - *6
+ - *8
- *10
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: password_digest
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': true
default:
default_function:
@@ -1062,8 +1299,8 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: role
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': false
default: member
default_function:
@@ -1071,16 +1308,16 @@ columns:
comment:
- *9
watches:
- - *22
- - *5
- - *6
+ - *23
+ - *7
+ - *8
- *9
- - *28
+ - *18
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: watching
- cast_type: *32
- sql_type_metadata: *33
+ cast_type: *33
+ sql_type_metadata: *34
'null': false
default: true
default_function:
@@ -1097,7 +1334,7 @@ columns:
default_function:
collation:
comment:
- - *5
+ - *7
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: first_failure_at
@@ -1108,9 +1345,9 @@ columns:
default_function:
collation:
comment:
- - *6
+ - *8
- *9
- - &36 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ - &40 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: webhook_id
cast_type: *3
@@ -1121,7 +1358,7 @@ columns:
collation:
comment:
webhook_deliveries:
- - *5
+ - *7
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: event_id
@@ -1132,7 +1369,7 @@ columns:
default_function:
collation:
comment:
- - *6
+ - *8
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: request
@@ -1156,25 +1393,25 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: state
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': false
default:
default_function:
collation:
comment:
- *9
- - *36
+ - *40
webhooks:
- - *37
- - *23
- - *5
- - *6
+ - *41
+ - *24
+ - *7
+ - *8
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: name
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': true
default:
default_function:
@@ -1183,8 +1420,8 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: signing_secret
- cast_type: *7
- sql_type_metadata: *8
+ cast_type: *5
+ sql_type_metadata: *6
'null': false
default:
default_function:
@@ -1213,11 +1450,13 @@ columns:
comment:
primary_keys:
accesses: id
+ account_join_codes: id
accounts: id
action_text_rich_texts: id
active_storage_attachments: id
active_storage_blobs: id
active_storage_variant_records: id
+ ai_quotas: id
ar_internal_metadata: key
assignees_filters:
assigners_filters:
@@ -1234,6 +1473,8 @@ primary_keys:
collections_filters:
columns: id
comments: id
+ conversation_messages: id
+ conversations: id
creators_filters:
entropy_configurations: id
events: id
@@ -1242,10 +1483,12 @@ primary_keys:
mentions: id
notification_bundles: id
notifications: id
+ period_highlights: id
pins: id
push_subscriptions: id
reactions: id
schema_migrations: version
+ search_embeddings_vector_chunks00: rowid
search_queries: id
search_results: id
sessions: id
@@ -1253,6 +1496,7 @@ primary_keys:
taggings: id
tags: id
user_settings: id
+ user_weekly_highlights: id
users: id
watches: id
webhook_delinquency_trackers: id
@@ -1260,11 +1504,13 @@ primary_keys:
webhooks: id
data_sources:
accesses: true
+ account_join_codes: true
accounts: true
action_text_rich_texts: true
active_storage_attachments: true
active_storage_blobs: true
active_storage_variant_records: true
+ ai_quotas: true
ar_internal_metadata: true
assignees_filters: true
assigners_filters: true
@@ -1281,6 +1527,8 @@ data_sources:
collections_filters: true
columns: true
comments: true
+ conversation_messages: true
+ conversations: true
creators_filters: true
entropy_configurations: true
events: true
@@ -1289,10 +1537,12 @@ data_sources:
mentions: true
notification_bundles: true
notifications: true
+ period_highlights: true
pins: true
push_subscriptions: true
reactions: true
schema_migrations: true
+ search_embeddings_vector_chunks00: true
search_queries: true
search_results: true
sessions: true
@@ -1300,6 +1550,7 @@ data_sources:
taggings: true
tags: true
user_settings: true
+ user_weekly_highlights: true
users: true
watches: true
webhook_delinquency_trackers: true
@@ -1372,6 +1623,23 @@ indexes:
nulls_not_distinct:
comment:
valid: true
+ account_join_codes:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
+ table: account_join_codes
+ name: index_account_join_codes_on_code
+ unique: true
+ columns:
+ - code
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
accounts:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: accounts
@@ -1495,6 +1763,39 @@ indexes:
nulls_not_distinct:
comment:
valid: true
+ ai_quotas:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
+ table: ai_quotas
+ name: index_ai_quotas_on_reset_at
+ unique: false
+ columns:
+ - reset_at
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
+ - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
+ table: ai_quotas
+ name: index_ai_quotas_on_user_id
+ unique: false
+ columns:
+ - user_id
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
ar_internal_metadata: []
assignees_filters:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
@@ -1929,13 +2230,13 @@ indexes:
nulls_not_distinct:
comment:
valid: true
+ comments:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
- table: columns
- name: index_columns_on_collection_id_and_position
+ table: comments
+ name: index_comments_on_card_id
unique: false
columns:
- - collection_id
- - position
+ - card_id
lengths: {}
orders: {}
opclasses: {}
@@ -1946,13 +2247,30 @@ indexes:
nulls_not_distinct:
comment:
valid: true
- comments:
+ conversation_messages:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
- table: comments
- name: index_comments_on_card_id
+ table: conversation_messages
+ name: index_conversation_messages_on_conversation_id
unique: false
columns:
- - card_id
+ - conversation_id
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
+ conversations:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
+ table: conversations
+ name: index_conversations_on_user_id
+ unique: true
+ columns:
+ - user_id
lengths: {}
orders: {}
opclasses: {}
@@ -2340,6 +2658,23 @@ indexes:
nulls_not_distinct:
comment:
valid: true
+ period_highlights:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
+ table: period_highlights
+ name: index_period_highlights_on_key_and_starts_at_and_duration
+ unique: true
+ columns:
+ - key
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
pins:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: pins
@@ -2508,6 +2843,7 @@ indexes:
comment:
valid: true
schema_migrations: []
+ search_embeddings_vector_chunks00: []
search_queries:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: search_queries
@@ -2696,6 +3032,56 @@ indexes:
nulls_not_distinct:
comment:
valid: true
+ user_weekly_highlights:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
+ table: user_weekly_highlights
+ name: index_user_weekly_highlights_on_period_highlights_id
+ unique: false
+ columns:
+ - period_highlights_id
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
+ - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
+ table: user_weekly_highlights
+ name: index_user_weekly_highlights_on_user_id
+ unique: false
+ columns:
+ - user_id
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
+ - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
+ table: user_weekly_highlights
+ name: index_user_weekly_highlights_on_user_id_and_starts_at
+ unique: true
+ columns:
+ - user_id
+ - starts_at
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
users:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: users
@@ -2845,4 +3231,4 @@ indexes:
nulls_not_distinct:
comment:
valid: true
-version: 20251029161222
+version: 20251016153034
diff --git a/db/seeds.rb b/db/seeds.rb
index 6311e6969..bf933467c 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -1,5 +1,5 @@
raise "Seeding is just for development" unless Rails.env.development?
-IdentityProvider.backend = IdentityProvider::Simple
+IdentityProvider.backend = IdentityProvider::LocalBackend
require "active_support/testing/time_helpers"
include ActiveSupport::Testing::TimeHelpers
diff --git a/db/untenanted_schema.rb b/db/untenanted_schema.rb
index d2937897b..7137cf2c8 100644
--- a/db/untenanted_schema.rb
+++ b/db/untenanted_schema.rb
@@ -10,24 +10,35 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.2].define(version: 2025_09_24_190729) do
+ActiveRecord::Schema[8.2].define(version: 2025_10_15_173452) do
create_table "identities", force: :cascade do |t|
t.datetime "created_at", null: false
+ t.string "email_address"
t.datetime "updated_at", null: false
+ t.index ["email_address"], name: "index_identities_on_email_address", unique: true
+ end
+
+ create_table "magic_links", force: :cascade do |t|
+ t.string "code", null: false
+ t.datetime "created_at", null: false
+ t.datetime "expires_at", null: false
+ t.integer "identity_id"
+ t.datetime "updated_at", null: false
+ t.index ["code"], name: "index_magic_links_on_code", unique: true
+ t.index ["expires_at"], name: "index_magic_links_on_expires_at"
+ t.index ["identity_id"], name: "index_magic_links_on_identity_id"
end
create_table "memberships", force: :cascade do |t|
t.string "account_name", null: false
t.datetime "created_at", null: false
- t.string "email_address", null: false
t.integer "identity_id", null: false
+ t.string "tenant", null: false
t.datetime "updated_at", null: false
- t.integer "user_id", null: false
- t.string "user_tenant", null: false
- t.index ["email_address"], name: "index_memberships_on_email_address"
t.index ["identity_id"], name: "index_memberships_on_identity_id"
- t.index ["user_tenant", "user_id"], name: "index_memberships_on_user_tenant_and_user_id"
+ t.index ["tenant"], name: "index_memberships_on_user_tenant_and_user_id"
end
+ add_foreign_key "magic_links", "identities"
add_foreign_key "memberships", "identities"
end
diff --git a/gems/fizzy-saas/app/controllers/identities_controller.rb b/gems/fizzy-saas/app/controllers/identities_controller.rb
index 2520ccbce..3459eabbc 100644
--- a/gems/fizzy-saas/app/controllers/identities_controller.rb
+++ b/gems/fizzy-saas/app/controllers/identities_controller.rb
@@ -2,22 +2,22 @@ class IdentitiesController < ApplicationController
include InternalApi
def link
- IdentityProvider::Simple.link(email_address: params[:email_address], to: params[:to])
+ IdentityProvider::LocalBackend.link(email_address: params[:email_address], to: params[:to])
head :ok
end
def unlink
- IdentityProvider::Simple.unlink(email_address: params[:email_address], from: params[:from])
+ IdentityProvider::LocalBackend.unlink(email_address: params[:email_address], from: params[:from])
head :ok
end
def change_email_address
- IdentityProvider::Simple.change_email_address(from: params[:from], to: params[:to], tenant: params[:tenant])
+ IdentityProvider::LocalBackend.change_email_address(from: params[:from], to: params[:to], tenant: params[:tenant])
head :ok
end
def send_magic_link
- code = IdentityProvider::Simple.send_magic_link(params[:email_address])
+ code = IdentityProvider::LocalBackend.send_magic_link(params[:email_address])
render json: { code: code }
end
end
diff --git a/gems/fizzy-saas/app/controllers/signups/completions_controller.rb b/gems/fizzy-saas/app/controllers/signups/completions_controller.rb
index c7bf66a3e..c33433a79 100644
--- a/gems/fizzy-saas/app/controllers/signups/completions_controller.rb
+++ b/gems/fizzy-saas/app/controllers/signups/completions_controller.rb
@@ -2,7 +2,7 @@ class Signups::CompletionsController < ApplicationController
include Restricted
require_untenanted_access
- before_action :require_identity
+ require_identified_access
def new
@signup = Signup.new
diff --git a/gems/fizzy-saas/app/controllers/signups_controller.rb b/gems/fizzy-saas/app/controllers/signups_controller.rb
index f837345fb..acfa52dc8 100644
--- a/gems/fizzy-saas/app/controllers/signups_controller.rb
+++ b/gems/fizzy-saas/app/controllers/signups_controller.rb
@@ -2,6 +2,7 @@ class SignupsController < ApplicationController
include Restricted
require_untenanted_access
+ require_unidentified_access
rate_limit only: :create, name: "short-term", to: 5, within: 3.minutes,
with: -> { redirect_to saas.new_signup_path, alert: "Try again later." }
diff --git a/gems/fizzy-saas/app/models/identity_provider/remote_backend.rb b/gems/fizzy-saas/app/models/identity_provider/remote_backend.rb
new file mode 100644
index 000000000..839c2919c
--- /dev/null
+++ b/gems/fizzy-saas/app/models/identity_provider/remote_backend.rb
@@ -0,0 +1,48 @@
+module IdentityProvider::RemoteBackend
+ extend self
+ include Fizzy::Saas::Engine.routes.url_helpers
+
+ delegate :consume_magic_link, :token_for, :resolve_token, :verify_token, :tenants_for, to: IdentityProvider::LocalBackend
+
+ def default_url_options
+ Rails.application.config.action_mailer.default_url_options
+ end
+
+ def url_options
+ default_url_options.merge(script_name: nil)
+ end
+
+ def link(email_address:, to:)
+ response = InternalApiClient.new(link_identity_url).post({ email_address: email_address, to: to })
+
+ unless response.success?
+ raise IdentityProvider::Error, "Failed to link identity: #{response.error || response.code}"
+ end
+ end
+
+ def unlink(email_address:, from:)
+ response = InternalApiClient.new(unlink_identity_url).post({ email_address: email_address, from: from })
+
+ unless response.success?
+ raise IdentityProvider::Error, "Failed to unlink identity: #{response.error || response.code}"
+ end
+ end
+
+ def change_email_address(from:, to:, tenant:)
+ response = InternalApiClient.new(change_identity_email_address_url).post({ from: from, to: to, tenant: tenant })
+
+ unless response.success?
+ raise IdentityProvider::Error, "Failed to change email address: #{response.error || response.code}"
+ end
+ end
+
+ def send_magic_link(email_address)
+ response = InternalApiClient.new(send_magic_link_url).post({ email_address: email_address })
+
+ if response.success?
+ response.parsed_body["code"]
+ else
+ raise IdentityProvider::Error, "Failed to send magic link: #{response.error || response.code}"
+ end
+ end
+end
diff --git a/gems/fizzy-saas/app/models/identity_provider/saas.rb b/gems/fizzy-saas/app/models/identity_provider/saas.rb
deleted file mode 100644
index d3b74fb4c..000000000
--- a/gems/fizzy-saas/app/models/identity_provider/saas.rb
+++ /dev/null
@@ -1,83 +0,0 @@
-module IdentityProvider::Saas
- class Error < StandardError; end
-
- extend self
- include Fizzy::Saas::Engine.routes.url_helpers
-
- def default_url_options
- Rails.application.config.action_mailer.default_url_options
- end
-
- def url_options
- default_url_options.merge(script_name: nil)
- end
-
- def link(email_address:, to:)
- response = InternalApiClient.new(link_identity_url).post({ email_address: email_address, to: to })
-
- unless response.success?
- raise Error, "Failed to link identity: #{response.error || response.code}"
- end
- end
-
- def unlink(email_address:, from:)
- response = InternalApiClient.new(unlink_identity_url).post({ email_address: email_address, from: from })
-
- unless response.success?
- raise Error, "Failed to unlink identity: #{response.error || response.code}"
- end
- end
-
- def change_email_address(from:, to:, tenant:)
- response = InternalApiClient.new(change_identity_email_address_url).post({ from: from, to: to, tenant: tenant })
-
- unless response.success?
- raise Error, "Failed to change email address: #{response.error || response.code}"
- end
- end
-
- def send_magic_link(email_address)
- response = InternalApiClient.new(send_magic_link_url).post({ email_address: email_address })
-
- if response.success?
- response.parsed_body["code"]
- else
- raise Error, "Failed to send magic link: #{response.error || response.code}"
- end
- end
-
- def consume_magic_link(code)
- identity = MagicLink.consume(code)
- wrap_identity(identity)
- end
-
- def token_for(email_address)
- identity = Identity.find_by(email_address: email_address)
- wrap_identity(identity)
- end
-
- def resolve_token(token)
- identity = Identity.find_signed(token&.dig("id"))
- identity&.email_address
- end
-
- def verify_token(token)
- identity = Identity.find_signed(token&.dig("id"))
- wrap_identity(identity)
- end
-
- def tenants_for(token)
- Identity.find_signed(token&.dig("id")).memberships.pluck(:tenant, :account_name).map do |id, name|
- IdentityProvider::Tenant.new(id: id, name: name)
- end
- end
-
- private
- def wrap_identity(identity)
- if identity
- IdentityProvider::Token.new(identity.signed_id, identity.updated_at)
- else
- nil
- end
- end
-end
diff --git a/gems/fizzy-saas/app/models/internal_api_client.rb b/gems/fizzy-saas/app/models/internal_api_client.rb
index 093fa75db..a07c74d79 100644
--- a/gems/fizzy-saas/app/models/internal_api_client.rb
+++ b/gems/fizzy-saas/app/models/internal_api_client.rb
@@ -3,7 +3,7 @@ class InternalApiClient
TOKEN_KEY = "internal_api_client_token"
USER_AGENT = "fizzy/1.0.0 InternalApiClient"
SIGNATURE_HEADER = "X-Internal-Signature"
- DEFAULT_TIMEOUT = 5.seconds
+ DEFAULT_TIMEOUT = 60.seconds
class Error < StandardError; end
class TimeoutError < Error; end
diff --git a/gems/fizzy-saas/lib/fizzy/saas/engine.rb b/gems/fizzy-saas/lib/fizzy/saas/engine.rb
index db71a4b0b..b9f3f71ac 100644
--- a/gems/fizzy-saas/lib/fizzy/saas/engine.rb
+++ b/gems/fizzy-saas/lib/fizzy/saas/engine.rb
@@ -5,7 +5,7 @@ module Fizzy
Queenbee.host_app = Fizzy
config.to_prepare do
- IdentityProvider.backend = IdentityProvider::Saas
+ IdentityProvider.backend = IdentityProvider::RemoteBackend
Queenbee::Subscription.short_names = Subscription::SHORT_NAMES
Queenbee::ApiToken.token = Rails.application.credentials.dig(:queenbee_api_token)
diff --git a/gems/fizzy-saas/test/controllers/signups/completions_controller_test.rb b/gems/fizzy-saas/test/controllers/signups/completions_controller_test.rb
index f750e389c..f21f3666a 100644
--- a/gems/fizzy-saas/test/controllers/signups/completions_controller_test.rb
+++ b/gems/fizzy-saas/test/controllers/signups/completions_controller_test.rb
@@ -33,7 +33,8 @@ class Signups::CompletionsControllerTest < ActionDispatch::IntegrationTest
}, headers: http_basic_auth_headers
end
- assert_redirected_to session_login_menu_path(go_to: Membership.last.tenant), "Successful completion should redirect to login menu"
+ tenant = Membership.last.tenant
+ assert_redirected_to new_session_start_url(script_name: "/#{tenant}"), "Successful completion should redirect to start session in new tenant"
post saas.signup_completion_path, params: {
signup: {
diff --git a/gems/fizzy-saas/test/models/identity_provider/saas_test.rb b/gems/fizzy-saas/test/models/identity_provider/remote_backend_test.rb
similarity index 65%
rename from gems/fizzy-saas/test/models/identity_provider/saas_test.rb
rename to gems/fizzy-saas/test/models/identity_provider/remote_backend_test.rb
index d4cd6f22f..6cb721af2 100644
--- a/gems/fizzy-saas/test/models/identity_provider/saas_test.rb
+++ b/gems/fizzy-saas/test/models/identity_provider/remote_backend_test.rb
@@ -1,32 +1,32 @@
require "test_helper"
-class IdentityProvider::SaasTest < ActiveSupport::TestCase
+class IdentityProvider::RemoteBackendTest < ActiveSupport::TestCase
setup do
WebMock.stub_request(:post, %r{http://example\.com(:80)?/identities/link})
.to_return do |request|
body = JSON.parse(request.body)
- IdentityProvider::Simple.link(email_address: body["email_address"], to: body["to"])
+ IdentityProvider::LocalBackend.link(email_address: body["email_address"], to: body["to"])
{ status: 200, body: {}.to_json, headers: { "Content-Type" => "application/json" } }
end
WebMock.stub_request(:post, %r{http://example\.com(:80)?/identities/unlink})
.to_return do |request|
body = JSON.parse(request.body)
- IdentityProvider::Simple.unlink(email_address: body["email_address"], from: body["from"])
+ IdentityProvider::LocalBackend.unlink(email_address: body["email_address"], from: body["from"])
{ status: 200, body: {}.to_json, headers: { "Content-Type" => "application/json" } }
end
WebMock.stub_request(:post, %r{http://example\.com(:80)?/identities/change_email_address})
.to_return do |request|
body = JSON.parse(request.body)
- IdentityProvider::Simple.change_email_address(from: body["from"], to: body["to"], tenant: body["tenant"])
+ IdentityProvider::LocalBackend.change_email_address(from: body["from"], to: body["to"], tenant: body["tenant"])
{ status: 200, body: {}.to_json, headers: { "Content-Type" => "application/json" } }
end
WebMock.stub_request(:post, %r{http://example\.com(:80)?/identities/send_magic_link})
.to_return do |request|
body = JSON.parse(request.body)
- code = IdentityProvider::Simple.send_magic_link(body["email_address"])
+ code = IdentityProvider::LocalBackend.send_magic_link(body["email_address"])
{ status: 200, body: { code: code }.to_json, headers: { "Content-Type" => "application/json" } }
end
end
@@ -37,7 +37,7 @@ class IdentityProvider::SaasTest < ActiveSupport::TestCase
assert_difference -> { Identity.count }, 1 do
assert_difference -> { Membership.count }, 1 do
- IdentityProvider::Saas.link(email_address: new_email, to: tenant)
+ IdentityProvider::RemoteBackend.link(email_address: new_email, to: tenant)
end
end
@@ -50,7 +50,7 @@ class IdentityProvider::SaasTest < ActiveSupport::TestCase
tenant = ApplicationRecord.current_tenant
assert_difference -> { Membership.count }, -1 do
- IdentityProvider::Saas.unlink(email_address: identity.email_address, from: tenant)
+ IdentityProvider::RemoteBackend.unlink(email_address: identity.email_address, from: tenant)
end
end
@@ -59,7 +59,7 @@ class IdentityProvider::SaasTest < ActiveSupport::TestCase
tenant = ApplicationRecord.current_tenant
new_email = "newemail@example.com"
- IdentityProvider::Saas.change_email_address(from: identity.email_address, to: new_email, tenant: tenant)
+ IdentityProvider::RemoteBackend.change_email_address(from: identity.email_address, to: new_email, tenant: tenant)
new_identity = Identity.find_by(email_address: new_email)
assert new_identity.memberships.exists?(tenant: tenant)
@@ -69,11 +69,11 @@ class IdentityProvider::SaasTest < ActiveSupport::TestCase
identity = identities(:kevin)
assert_difference -> { MagicLink.count }, 1 do
- code = IdentityProvider::Saas.send_magic_link(identity.email_address)
+ code = IdentityProvider::RemoteBackend.send_magic_link(identity.email_address)
assert_equal MagicLink::CODE_LENGTH, code.length
end
- code = IdentityProvider::Saas.send_magic_link("nonexistent@example.com")
+ code = IdentityProvider::RemoteBackend.send_magic_link("nonexistent@example.com")
assert_nil code
end
@@ -81,21 +81,21 @@ class IdentityProvider::SaasTest < ActiveSupport::TestCase
identity = identities(:kevin)
magic_link = identity.send_magic_link
- token = IdentityProvider::Saas.consume_magic_link(magic_link.code)
+ token = IdentityProvider::RemoteBackend.consume_magic_link(magic_link.code)
assert_equal identity.signed_id, token.id
assert_not MagicLink.exists?(magic_link.id)
- token = IdentityProvider::Saas.consume_magic_link("invalid")
+ token = IdentityProvider::RemoteBackend.consume_magic_link("invalid")
assert_nil token
end
test "token_for" do
identity = identities(:kevin)
- token = IdentityProvider::Saas.token_for(identity.email_address)
+ token = IdentityProvider::RemoteBackend.token_for(identity.email_address)
assert_equal identity.signed_id, token.id
- token = IdentityProvider::Saas.token_for("nonexistent@example.com")
+ token = IdentityProvider::RemoteBackend.token_for("nonexistent@example.com")
assert_nil token
end
@@ -103,10 +103,10 @@ class IdentityProvider::SaasTest < ActiveSupport::TestCase
identity = identities(:kevin)
token = { "id" => identity.signed_id }
- email = IdentityProvider::Saas.resolve_token(token)
+ email = IdentityProvider::RemoteBackend.resolve_token(token)
assert_equal identity.email_address, email
- email = IdentityProvider::Saas.resolve_token({ "id" => "invalid" })
+ email = IdentityProvider::RemoteBackend.resolve_token({ "id" => "invalid" })
assert_nil email
end
@@ -114,10 +114,10 @@ class IdentityProvider::SaasTest < ActiveSupport::TestCase
identity = identities(:kevin)
token = { "id" => identity.signed_id }
- result = IdentityProvider::Saas.verify_token(token)
+ result = IdentityProvider::RemoteBackend.verify_token(token)
assert_equal identity.signed_id, result.id
- result = IdentityProvider::Saas.verify_token({ "id" => "invalid" })
+ result = IdentityProvider::RemoteBackend.verify_token({ "id" => "invalid" })
assert_nil result
end
@@ -125,7 +125,7 @@ class IdentityProvider::SaasTest < ActiveSupport::TestCase
identity = identities(:kevin)
token = { "id" => identity.signed_id }
- tenants = IdentityProvider::Saas.tenants_for(token)
+ tenants = IdentityProvider::RemoteBackend.tenants_for(token)
assert tenants.all? { |t| t.is_a?(IdentityProvider::Tenant) }
assert_includes tenants.map(&:id), identity.memberships.first.tenant
end
diff --git a/script/create-identities.rb b/script/create-identities.rb
new file mode 100644
index 000000000..2411f1b0c
--- /dev/null
+++ b/script/create-identities.rb
@@ -0,0 +1,23 @@
+#!/usr/bin/env ruby
+
+require_relative "../config/environment"
+
+# Loop through all tenants and create identities for users
+ApplicationRecord.with_each_tenant do |tenant|
+ puts "Processing tenant: #{tenant}"
+
+ User.find_each do |user|
+ next if user.system?
+
+ # Use IdentityProvider to link the user's email to this tenant
+ # This will find_or_create the identity and link it to the tenant
+ identity = IdentityProvider.link(email_address: user.email_address, to: tenant)
+
+ puts " ✅ Linked identity for user #{user.id} (#{user.email_address}) to tenant '#{tenant}'"
+ end
+
+ puts " Completed tenant: #{tenant}"
+ puts
+end
+
+puts "All identities created successfully!"
diff --git a/test/models/identity_provider/simple_test.rb b/test/models/identity_provider/local_backend_test.rb
similarity index 69%
rename from test/models/identity_provider/simple_test.rb
rename to test/models/identity_provider/local_backend_test.rb
index 4a246a207..e760cefd7 100644
--- a/test/models/identity_provider/simple_test.rb
+++ b/test/models/identity_provider/local_backend_test.rb
@@ -1,13 +1,13 @@
require "test_helper"
-class IdentityProvider::SimpleTest < ActiveSupport::TestCase
+class IdentityProvider::LocalBackendTest < ActiveSupport::TestCase
test "link" do
new_email = "newuser@example.com"
tenant = ApplicationRecord.current_tenant
assert_difference -> { Identity.count }, 1 do
assert_difference -> { Membership.count }, 1 do
- IdentityProvider::Simple.link(email_address: new_email, to: tenant)
+ IdentityProvider::LocalBackend.link(email_address: new_email, to: tenant)
end
end
@@ -20,7 +20,7 @@ class IdentityProvider::SimpleTest < ActiveSupport::TestCase
tenant = ApplicationRecord.current_tenant
assert_difference -> { Membership.count }, -1 do
- IdentityProvider::Simple.unlink(email_address: identity.email_address, from: tenant)
+ IdentityProvider::LocalBackend.unlink(email_address: identity.email_address, from: tenant)
end
assert_not identity.reload.memberships.exists?(tenant: tenant), "removes membership from tenant"
@@ -31,7 +31,7 @@ class IdentityProvider::SimpleTest < ActiveSupport::TestCase
tenant = ApplicationRecord.current_tenant
new_email = "newemail@example.com"
- IdentityProvider::Simple.change_email_address(from: identity.email_address, to: new_email, tenant: tenant)
+ IdentityProvider::LocalBackend.change_email_address(from: identity.email_address, to: new_email, tenant: tenant)
assert_not identity.reload.memberships.exists?(tenant: tenant), "removes old identity membership"
new_identity = Identity.find_by(email_address: new_email)
@@ -42,11 +42,11 @@ class IdentityProvider::SimpleTest < ActiveSupport::TestCase
identity = identities(:kevin)
assert_difference -> { MagicLink.count }, 1 do
- code = IdentityProvider::Simple.send_magic_link(identity.email_address)
+ code = IdentityProvider::LocalBackend.send_magic_link(identity.email_address)
assert_equal MagicLink::CODE_LENGTH, code.length, "returns code of correct length"
end
- code = IdentityProvider::Simple.send_magic_link("nonexistent@example.com")
+ code = IdentityProvider::LocalBackend.send_magic_link("nonexistent@example.com")
assert_nil code, "returns nil for non-existent email"
end
@@ -54,21 +54,21 @@ class IdentityProvider::SimpleTest < ActiveSupport::TestCase
identity = identities(:kevin)
magic_link = identity.send_magic_link
- token = IdentityProvider::Simple.consume_magic_link(magic_link.code)
+ token = IdentityProvider::LocalBackend.consume_magic_link(magic_link.code)
assert_equal identity.signed_id, token.id, "returns token for valid code"
assert_not MagicLink.exists?(magic_link.id), "deletes magic link after consumption"
- token = IdentityProvider::Simple.consume_magic_link("invalid")
+ token = IdentityProvider::LocalBackend.consume_magic_link("invalid")
assert_nil token, "returns nil for invalid code"
end
test "token_for" do
identity = identities(:kevin)
- token = IdentityProvider::Simple.token_for(identity.email_address)
+ token = IdentityProvider::LocalBackend.token_for(identity.email_address)
assert_equal identity.signed_id, token.id, "returns token for existing email"
- token = IdentityProvider::Simple.token_for("nonexistent@example.com")
+ token = IdentityProvider::LocalBackend.token_for("nonexistent@example.com")
assert_nil token, "returns nil for non-existent email"
end
@@ -76,10 +76,10 @@ class IdentityProvider::SimpleTest < ActiveSupport::TestCase
identity = identities(:kevin)
token = { "id" => identity.signed_id }
- email = IdentityProvider::Simple.resolve_token(token)
+ email = IdentityProvider::LocalBackend.resolve_token(token)
assert_equal identity.email_address, email, "returns email address from valid token"
- email = IdentityProvider::Simple.resolve_token({ "id" => "invalid" })
+ email = IdentityProvider::LocalBackend.resolve_token({ "id" => "invalid" })
assert_nil email, "returns nil for invalid token"
end
@@ -87,10 +87,10 @@ class IdentityProvider::SimpleTest < ActiveSupport::TestCase
identity = identities(:kevin)
token = { "id" => identity.signed_id }
- result = IdentityProvider::Simple.verify_token(token)
+ result = IdentityProvider::LocalBackend.verify_token(token)
assert_equal identity.signed_id, result.id, "returns token from valid token hash"
- result = IdentityProvider::Simple.verify_token({ "id" => "invalid" })
+ result = IdentityProvider::LocalBackend.verify_token({ "id" => "invalid" })
assert_nil result, "returns nil for invalid token"
end
@@ -98,7 +98,7 @@ class IdentityProvider::SimpleTest < ActiveSupport::TestCase
identity = identities(:kevin)
token = { "id" => identity.signed_id }
- tenants = IdentityProvider::Simple.tenants_for(token)
+ tenants = IdentityProvider::LocalBackend.tenants_for(token)
assert tenants.all? { |t| t.is_a?(IdentityProvider::Tenant) }, "returns Tenant objects"
assert_includes tenants.map(&:id), identity.memberships.first.tenant, "includes identity's tenant"
end
diff --git a/test/models/user/email_address_changeable_test.rb b/test/models/user/email_address_changeable_test.rb
index 3b372c336..744c30e57 100644
--- a/test/models/user/email_address_changeable_test.rb
+++ b/test/models/user/email_address_changeable_test.rb
@@ -1,14 +1,26 @@
require "test_helper"
class User::EmailAddressChangeableTest < ActiveSupport::TestCase
- test "generate_email_address_change_token" do
+ test "send_email_address_change_confirmation" do
user = users(:david)
new_email_address = "new@example.com"
- token = user.generate_email_address_change_token(to: new_email_address)
+ assert_enqueued_jobs 1, only: ActionMailer::MailDeliveryJob do
+ user.send_email_address_change_confirmation(new_email_address)
+ end
+
+ assert_not_equal new_email_address, user.reload.email_address
+ end
+
+ test "generate_email_address_change_token" do
+ user = users(:david)
+ old_email = user.email_address
+ new_email = "david.new@37signals.com"
+
+ token = user.generate_email_address_change_token(to: new_email, expires_in: 30.minutes)
assert_kind_of String, token
- assert_not_equal new_email_address, user.reload.email_address
+ assert token.present?
end
test "change_email_address_using_token" do
@@ -16,7 +28,7 @@ class User::EmailAddressChangeableTest < ActiveSupport::TestCase
old_email = user.email_address
new_email = "david.new@37signals.com"
- token = user.generate_email_address_change_token(from: old_email, to: new_email)
+ token = user.generate_email_address_change_token(to: new_email, expires_in: 30.minutes)
assert_equal old_email, user.reload.email_address
diff --git a/test/models/user/identifiable_test.rb b/test/models/user/identifiable_test.rb
index b842632fd..28d18624b 100644
--- a/test/models/user/identifiable_test.rb
+++ b/test/models/user/identifiable_test.rb
@@ -12,20 +12,6 @@ class User::IdentifiableTest < ActiveSupport::TestCase
assert_equal "newuser@example.com", user.identity.email_address
end
- test "update email address" do
- user = users(:david)
- old_email = user.email_address
- new_email = "david.updated@example.com"
-
- assert_not Identity.find_by(email_address: new_email)
-
- user.update!(email_address: new_email)
-
- new_identity = Identity.find_by(email_address: new_email)
- assert new_identity.present?
- assert new_identity.memberships.exists?(tenant: user.tenant)
- end
-
test "destroy" do
user = User.create!(name: "Bob")
diff --git a/test/system/smoke_test.rb b/test/system/smoke_test.rb
index 51cb42c45..16e6ded73 100644
--- a/test/system/smoke_test.rb
+++ b/test/system/smoke_test.rb
@@ -43,7 +43,37 @@ class SmokeTest < ApplicationSystemTestCase
private
def sign_in_as(user)
- visit session_transfer_url(user.transfer_id)
+ # Visit app first to establish domain for cookies
+ visit root_url
+
+ identity = Identity.find_or_create_by!(email_address: user.email_address)
+ identity.link_to(user.tenant)
+
+ session = user.sessions.create!(user_agent: "Test", ip_address: "127.0.0.1")
+
+ secret_key_base = Rails.application.secret_key_base
+ key_generator = ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
+ secret = key_generator.generate_key("signed cookie")
+ verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON)
+
+ identity_token = IdentityProvider::Token.new(identity.signed_id, identity.updated_at)
+ signed_identity_token = verifier.generate(identity_token.to_h)
+ signed_session_token = verifier.generate(session.signed_id)
+
+ # Set cookies in browser
+ Capybara.current_session.driver.browser.manage.add_cookie(
+ name: "identity_token",
+ value: signed_identity_token,
+ path: "/"
+ )
+
+ Capybara.current_session.driver.browser.manage.add_cookie(
+ name: "session_token",
+ value: signed_session_token,
+ path: "/#{user.tenant}"
+ )
+
+ visit root_url
assert_selector "h1", text: "Activity"
end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 63e6262fa..912154ef4 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -66,4 +66,4 @@ unless Rails.application.config.x.oss_config
load File.expand_path("../gems/fizzy-saas/test/test_helper.rb", __dir__)
end
-IdentityProvider.backend = IdentityProvider::Simple
+IdentityProvider.backend = IdentityProvider::LocalBackend