Introduce an "Identity" model to ease login

- New untenanted Identity and Membership models
- New `identity_token` cookie with path "/" holds state across tenants

We're not sure whether the untenanted database will be sqlite or
MySQL, and so I've been careful to minimize

- database reads, placing them behind etags and caching
- database writes, only writing when a new Session is created (login)

Note that we track two things in the identity_token cookie: a signed
id, and the updated_at for the underlying Identity object. This allows
us to effectively cache on the Identity without having to hit the
database, by using an Identity::Mock object that is compatible with
etag and cache methods.

The new integration test shows the desired user-facing behavior, which
is to make it easy to login without a tenanted URL and to jump between
tenants.

- the untenanted "login_help" page shows all linked memberships
- the jump menu shows all linked memberships (except the current)

Also introduced a utility script to populate existing employee
Identities, grouping accounts by email address.
This commit is contained in:
Mike Dalessio
2025-10-09 17:23:31 -04:00
parent f38410890a
commit 3399e45130
24 changed files with 336 additions and 32 deletions
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env ruby
require_relative "../../config/environment"
ApplicationRecord.with_each_tenant do |tenant|
puts "# #{tenant}"
User.find_each do |user|
next if user.system? || !user.active?
if user.membership.present?
puts "Found identity #{user.identity.id} for user #{user.id} (#{user.email_address})"
else
memberships = Membership.where(email_address: user.email_address)
if memberships.empty?
# Create a new Identity
Identity.transaction do
identity = Identity.create!
user.membership = identity.memberships.create!(user_id: user.id, user_tenant: user.tenant, email_address: user.email_address, account_name: Account.sole.name)
puts "Created identity #{identity.id} for user #{user.id} (#{user.email_address})"
end
else
# Merge this User's Membership into the existing Identity
identity = memberships.first.identity
user.membership = identity.memberships.create!(user_id: user.id, user_tenant: user.tenant, email_address: user.email_address, account_name: Account.sole.name)
puts "Merged membership for user #{user.id} (#{user.email_address}) into identity #{identity.id}"
end
end
end
end