Drop memberships
This commit is contained in:
committed by
Mike Dalessio
parent
5c6b91ef77
commit
edf837fed3
@@ -9,8 +9,9 @@ module ApplicationCable
|
||||
private
|
||||
def set_current_user
|
||||
if session = find_session_by_cookie
|
||||
membership = session.identity.memberships.find_by!(tenant: request.env["fizzy.external_account_id"])
|
||||
self.current_user = membership.user if membership.user.active?
|
||||
account = Account.find(request.env["fizzy.external_account_id"])
|
||||
user = session.identity.user.find_by!(account: account)
|
||||
self.current_user = user if user.active?
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ module Authorization
|
||||
end
|
||||
|
||||
def ensure_can_access_account
|
||||
redirect_to session_menu_url(script_name: nil) if Current.membership.blank?
|
||||
redirect_to session_menu_url(script_name: nil) if Current.user.blank? || !Current.user.active?
|
||||
end
|
||||
|
||||
def redirect_existing_user
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
class Memberships::EmailAddresses::ConfirmationsController < ApplicationController
|
||||
class Users::EmailAddresses::ConfirmationsController < ApplicationController
|
||||
disallow_account_scope
|
||||
allow_unauthenticated_access
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
class Memberships::EmailAddressesController < ApplicationController
|
||||
class Users::EmailAddressesController < ApplicationController
|
||||
disallow_account_scope
|
||||
|
||||
layout "public"
|
||||
@@ -1,6 +1,6 @@
|
||||
module ApplicationHelper
|
||||
def page_title_tag
|
||||
account_name = if Current.account && Current.session&.identity&.memberships&.many?
|
||||
account_name = if Current.account && Current.session&.identity&.users&.many?
|
||||
Current.account&.name
|
||||
end
|
||||
tag.title [ @page_title, account_name, "Fizzy" ].compact.join(" | ")
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
class Current < ActiveSupport::CurrentAttributes
|
||||
attribute :session, :membership, :account
|
||||
attribute :session, :user, :account
|
||||
attribute :http_method, :request_id, :user_agent, :ip_address, :referrer
|
||||
|
||||
delegate :identity, to: :session, allow_nil: true
|
||||
delegate :user, to: :membership, allow_nil: true
|
||||
|
||||
def session=(value)
|
||||
super(value)
|
||||
|
||||
if value.present? && Current.account.present?
|
||||
self.membership = identity.memberships.find_by(tenant: Current.account.external_account_id)
|
||||
self.user = identity.users.find_by(account: Current.account)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
class Identity < ApplicationRecord
|
||||
include Joinable, Transferable
|
||||
|
||||
has_many :memberships, dependent: :destroy
|
||||
has_many :users, dependent: :nullify
|
||||
has_many :magic_links, dependent: :destroy
|
||||
has_many :sessions, dependent: :destroy
|
||||
|
||||
before_destroy :deactivate_users
|
||||
|
||||
normalizes :email_address, with: ->(value) { value.strip.downcase.presence }
|
||||
|
||||
def send_magic_link
|
||||
@@ -16,4 +18,9 @@ class Identity < ApplicationRecord
|
||||
def staff?
|
||||
email_address.ends_with?("@37signals.com") || email_address.ends_with?("@basecamp.com")
|
||||
end
|
||||
|
||||
private
|
||||
def deactivate_users
|
||||
users.find_each(&:deactivate)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
module Identity::Joinable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def join(account)
|
||||
def join(account, **attributes)
|
||||
attributes[:name] ||= email_address
|
||||
|
||||
transaction do
|
||||
membership = memberships.create!(tenant: account.external_account_id)
|
||||
account.users.create!(membership: membership, name: email_address)
|
||||
account.users.create!(**attributes, identity: self)
|
||||
end
|
||||
end
|
||||
|
||||
def member_of?(account)
|
||||
memberships.exists?(tenant: account.external_account_id.to_s)
|
||||
account.users.exists?(identity: self)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
class Membership < ApplicationRecord
|
||||
include EmailAddressChangeable
|
||||
|
||||
belongs_to :identity, touch: true
|
||||
|
||||
class << self
|
||||
def change_email_address(from:, to:, tenant:)
|
||||
identity = Identity.find_by(email_address: from)
|
||||
membership = find_by(tenant: tenant, identity: identity)
|
||||
|
||||
if membership
|
||||
new_identity = Identity.find_or_create_by!(email_address: to)
|
||||
membership.update!(identity: new_identity)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def account
|
||||
Account.find_by_external_account_id(tenant)
|
||||
end
|
||||
|
||||
def account_name
|
||||
account&.name
|
||||
end
|
||||
|
||||
def user
|
||||
# TODO:PLANB: should this find should be scoped by account?
|
||||
User.find_by(membership_id: id)
|
||||
end
|
||||
end
|
||||
+1
-4
@@ -6,9 +6,7 @@ class User < ApplicationRecord
|
||||
has_one_attached :avatar
|
||||
|
||||
belongs_to :account, default: -> { Current.account }
|
||||
belongs_to :membership, optional: true
|
||||
|
||||
has_one :identity, through: :membership, disable_joins: true
|
||||
belongs_to :identity, optional: true
|
||||
|
||||
has_many :comments, inverse_of: :creator, dependent: :destroy
|
||||
|
||||
@@ -22,7 +20,6 @@ class User < ApplicationRecord
|
||||
def deactivate
|
||||
transaction do
|
||||
accesses.destroy_all
|
||||
membership.destroy!
|
||||
update! active: false
|
||||
end
|
||||
end
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
module Membership::EmailAddressChangeable
|
||||
module User::EmailAddressChangeable
|
||||
EMAIL_CHANGE_TOKEN_PURPOSE = "change_email_address"
|
||||
EMAIL_CHANGE_TOKEN_EXPIRATION = 30.minutes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<p class="subtitle">Confirm your email address change</p>
|
||||
|
||||
<%= link_to "Yes use use this email address", email_address_confirmation_url(membership_id: @membership.id, email_address_token: @token), class: "btn" %>
|
||||
<%= link_to "Yes use use this email address", email_address_confirmation_url(user_id: @user.id, email_address_token: @token), class: "btn" %>
|
||||
|
||||
<p class="margin-block-start-double">If you didn’t request this change, you can ignore this email. Your email address WILL NOT be changed unless you hit the button.</p>
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@ Confirm your email address change
|
||||
|
||||
Hit the link below to use this email address in Fizzy:
|
||||
|
||||
<%= email_address_confirmation_url(membership_id: @membership.id, email_address_token: @token) %>
|
||||
<%= email_address_confirmation_url(user_id: @user.id, email_address_token: @token) %>
|
||||
|
||||
If you didn’t request this change, you can ignore this email. Your email address WILL NOT be changed unless you hit the button.
|
||||
If you didn’t request this change, you can ignore this email. Your email address WILL NOT be changed unless you hit the button.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
<% if @identity.memberships.any? %>
|
||||
<% if @identity.users.any? %>
|
||||
<h1 class="title">Fizzy verification code</h1>
|
||||
<p class="subtitle">Please enter this 6-character verification code on the Fizzy sign-in page:</p>
|
||||
<% else %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<% if @identity.memberships.any? %>
|
||||
<% if @identity.users.any? %>
|
||||
Please enter this 6-character verification code on the Fizzy sign-in page:
|
||||
<% else %>
|
||||
Please enter this 6-character verification code on the Fizzy sign-up page to create your new account:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="flex gap">
|
||||
<div class="flex flex-column full-width justify-center gap-half">
|
||||
<div class="nav__header">
|
||||
<% if Current.user.identity.memberships.many? %>
|
||||
<% if Current.user.identity.users.many? %>
|
||||
<div class="nav__header-title max-width">
|
||||
<div class="overflow-ellipsis"><strong><%= Current.account.name %></strong></div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<%= turbo_frame_tag Current.identity, :account_menu do %>
|
||||
<% cache [ Current.identity, @memberships ] do %>
|
||||
<% cache [ Current.identity, @accounts ] do %>
|
||||
<%= collapsible_nav_section "Accounts" do %>
|
||||
<% @memberships.each do |membership| %>
|
||||
<%= filter_place_menu_item landing_url(script_name: "/#{membership.tenant}"), "#{membership.account_name}", "marker", new_window: true %>
|
||||
<% @accounts.each do |account| %>
|
||||
<%= filter_place_menu_item landing_url(script_name: account.slug, account.name, "marker", new_window: true %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<% @page_title = "Choose an account" %>
|
||||
|
||||
<% cache [ Current.identity, @memberships ] do %>
|
||||
<% cache [ Current.identity, @accounts ] do %>
|
||||
<div
|
||||
class="panel panel--centered flex flex-column gap-half"
|
||||
style="--popup-icon-size: 24px; --popup-item-padding-inline: 0.5rem;"
|
||||
>
|
||||
<% if @memberships.present? %>
|
||||
<% if @accounts.any? %>
|
||||
<h1 class="txt-x-large font-weight-black margin-none">
|
||||
Your Fizzy accounts
|
||||
</h1>
|
||||
<menu class="popup__list pad border-radius border">
|
||||
<% @memberships.each do |membership| %>
|
||||
<% @accounts.each do |account| %>
|
||||
<li class="popup__item txt-medium">
|
||||
<%= icon_tag "marker", class: "popup__icon" %>
|
||||
<%= link_to landing_url(script_name: "/#{membership.tenant}"), class: "btn overflow-ellipsis fill-transparent popup__btn" do %>
|
||||
<strong><%= membership.account_name %></strong>
|
||||
<%= link_to landing_url(script_name: account.slug), class: "btn overflow-ellipsis fill-transparent popup__btn" do %>
|
||||
<strong><%= account.name %></strong>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<div class="flex align-center gap">
|
||||
<div class="flex align-center gap input input--actor">
|
||||
<%= form.email_field :email_address, class: "input full-width", autocomplete: "username", placeholder: "Email address", required: true, readonly: true, value: @user.identity.email_address %>
|
||||
<%= link_to "Change email", new_email_address_url(script_name: nil, membership_id: Current.membership.id), class: "btn btn--plain txt-link txt-small txt-nowrap" %>
|
||||
<%= link_to "Change email", new_email_address_url(script_name: nil, user_id: Current.user.id), class: "btn btn--plain txt-link txt-small txt-nowrap" %>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" id="log_in" class="btn btn--reversed center" data-form-target="submit">
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
<p class="margin-none">Just a sec while we confirm your new email address.</p>
|
||||
</header>
|
||||
|
||||
<%= form_with url: email_address_confirmation_path(membership_id: @membership.id), method: :post, data: { controller: "form auto-submit" } do |form| %>
|
||||
<%= form_with url: email_address_confirmation_path(user_id: @user.id), method: :post, data: { controller: "form auto-submit" } do |form| %>
|
||||
<%= form.hidden_field :email_address_token, value: params[:email_address_token] %>
|
||||
|
||||
<button type="submit" class="btn btn--link center" data-form-target="submit">
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
<% content_for :header do %>
|
||||
<div class="header__actions header__actions--start">
|
||||
<%= link_to edit_user_path(@membership.user, script_name: "/#{@tenant}"), class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %>
|
||||
<%= link_to edit_user_path(@user, script_name: @user.account.slug), class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %>
|
||||
<span class="overflow-ellipsis flex align-center">
|
||||
<strong>Back to My profile</strong>
|
||||
<kbd class="txt-x-small margin-inline-start hide-on-touch">←</kbd>
|
||||
@@ -16,5 +16,5 @@
|
||||
<p class="margin-none">We just sent an email to <strong><%= params[:email_address] %></strong></p>
|
||||
<p class="margin-none-block-start">Hit the link in the email to confirm this is the email address you want to use with Fizzy going forward.</p>
|
||||
|
||||
<%= link_to "Done", edit_user_path(@membership.user, script_name: "/#{@tenant}"), class: "btn btn--link center" %>
|
||||
<%= link_to "Done", edit_user_path(@user, script_name: @user.account.slug), class: "btn btn--link center" %>
|
||||
</div>
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
<% content_for :header do %>
|
||||
<div class="header__actions header__actions--start">
|
||||
<%= link_to edit_user_path(@membership.user, script_name: "/#{@membership.tenant}"), class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %>
|
||||
<%= link_to edit_user_path(@user, script_name: @user.account.slug), class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %>
|
||||
<span class="overflow-ellipsis flex align-center">
|
||||
<strong>Back to My profile</strong>
|
||||
<kbd class="txt-x-small margin-inline-start hide-on-touch">←</kbd>
|
||||
@@ -16,8 +16,8 @@
|
||||
<%= @page_title %>
|
||||
</h1>
|
||||
|
||||
<%= form_with url: email_addresses_path(membership_id: @membership.id), method: :post, class: "flex flex-column gap-half", data: { controller: "form", turbo: false } do |form| %>
|
||||
<div class="flex align-center gap-haf">
|
||||
<%= form_with url: user_email_addresses_path(user_id: @user.id), method: :post, class: "flex flex-column gap-half", data: { controller: "form", turbo: false } do |form| %>
|
||||
<div class="flex align-center gap-half">
|
||||
<label class="flex align-center gap input input--actor">
|
||||
<%= form.email_field :email_address, class: "input full-width", autocomplete: "email", placeholder: "New email address", autofocus: true, required: true %>
|
||||
</label>
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="panel panel--centered flex flex-column gap-half">
|
||||
<h1 class="txt-large font-weight-black margin-none"><%= @page_title %></h1>
|
||||
|
||||
<%= form_with scope: "user", url: users_joins_path(membership: params[:membership]), class: "flex flex-column gap txt-medium", data: { controller: "form" } do |form| %>
|
||||
<%= form_with scope: "user", url: users_joins_path, class: "flex flex-column gap txt-medium", data: { controller: "form" } do |form| %>
|
||||
<div class="flex align-center gap">
|
||||
<label class="flex align-center gap input input--actor">
|
||||
<%= form.text_field :name, class: "input full-width", autocomplete: "name", placeholder: "Full name", autofocus: true, required: true, data: { "1p-ignore": true } %>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
class DropMemberships < ActiveRecord::Migration[8.2]
|
||||
def change
|
||||
add_reference :users, :identity, type: :uuid, null: true, foreign_key: true
|
||||
remove_column :users, :membership_id, :bigint
|
||||
drop_table :memberships
|
||||
end
|
||||
end
|
||||
Generated
+4
-14
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.2].define(version: 2025_11_12_184932) do
|
||||
ActiveRecord::Schema[8.2].define(version: 2025_11_13_111501) do
|
||||
create_table "accesses", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.datetime "accessed_at"
|
||||
t.uuid "board_id", null: false
|
||||
@@ -288,16 +288,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_12_184932) do
|
||||
t.index ["identity_id"], name: "index_magic_links_on_identity_id"
|
||||
end
|
||||
|
||||
create_table "memberships", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "identity_id", null: false
|
||||
t.string "tenant", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["identity_id"], name: "index_memberships_on_identity_id"
|
||||
t.index ["tenant", "identity_id"], name: "index_memberships_on_tenant_and_identity_id", unique: true
|
||||
t.index ["tenant"], name: "index_memberships_on_user_tenant_and_user_id"
|
||||
end
|
||||
|
||||
create_table "mentions", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "mentionee_id", null: false
|
||||
@@ -632,11 +622,11 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_12_184932) do
|
||||
t.uuid "account_id"
|
||||
t.boolean "active", default: true, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "membership_id"
|
||||
t.uuid "identity_id"
|
||||
t.string "name", null: false
|
||||
t.string "role", default: "member", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["membership_id"], name: "index_users_on_membership_id"
|
||||
t.index ["identity_id"], name: "index_users_on_identity_id"
|
||||
t.index ["role"], name: "index_users_on_role"
|
||||
end
|
||||
|
||||
@@ -700,7 +690,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_12_184932) do
|
||||
add_foreign_key "comments", "cards"
|
||||
add_foreign_key "events", "boards"
|
||||
add_foreign_key "magic_links", "identities"
|
||||
add_foreign_key "memberships", "identities"
|
||||
add_foreign_key "mentions", "users", column: "mentionee_id"
|
||||
add_foreign_key "mentions", "users", column: "mentioner_id"
|
||||
add_foreign_key "notification_bundles", "users"
|
||||
@@ -715,6 +704,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_12_184932) do
|
||||
add_foreign_key "taggings", "cards"
|
||||
add_foreign_key "taggings", "tags"
|
||||
add_foreign_key "user_settings", "users"
|
||||
add_foreign_key "users", "identities"
|
||||
add_foreign_key "watches", "cards"
|
||||
add_foreign_key "watches", "users"
|
||||
add_foreign_key "webhook_delinquency_trackers", "webhooks"
|
||||
|
||||
@@ -113,7 +113,6 @@ class Import
|
||||
account = import.accounts.sole
|
||||
|
||||
new_identity = Identity.find_or_create_by!(email_address: membership.identity.email_address)
|
||||
new_membership = new_identity.memberships.find_or_create_by!(tenant: account.external_account_id.to_s)
|
||||
|
||||
if Account.all.exists?(external_account_id: account.external_account_id)
|
||||
raise "Account already exists"
|
||||
@@ -125,7 +124,7 @@ class Import
|
||||
},
|
||||
owner: {
|
||||
name: oldest_admin.name,
|
||||
membership_id: new_membership.id
|
||||
identity: new_identity
|
||||
}
|
||||
)
|
||||
@tenant = @account.external_account_id
|
||||
@@ -147,26 +146,28 @@ class Import
|
||||
step("Copying users", "Copied %{count} users in %{duration}") do
|
||||
mapping[:users] ||= {}
|
||||
import.users.find_each do |old_user|
|
||||
new_membership = nil
|
||||
new_identity = nil
|
||||
|
||||
if old_user.active && old_user.membership_id
|
||||
if old_user.membership_id
|
||||
membership = untenanted.memberships.find(old_user.membership_id)
|
||||
new_identity = Identity.find_or_create_by!(email_address: membership.identity.email_address)
|
||||
new_membership = new_identity.memberships.find_or_create_by!(tenant: tenant)
|
||||
end
|
||||
|
||||
new_user = User.find_or_create_by!(account_id: account.id, membership_id: new_membership&.id) do |u|
|
||||
u.name = old_user.name
|
||||
u.role = old_user.role
|
||||
u.active = old_user.active
|
||||
end
|
||||
new_user = User.create!(
|
||||
account: account,
|
||||
identity: new_identity,
|
||||
name: old_user.name,
|
||||
role: old_user.role,
|
||||
active: old_user.active,
|
||||
)
|
||||
|
||||
old_settings = old_user.settings
|
||||
if old_settings
|
||||
User::Settings.find_or_create_by!(user_id: new_user.id) do |s|
|
||||
s.bundle_email_frequency = old_settings.bundle_email_frequency
|
||||
s.timezone_name = old_settings.timezone_name
|
||||
end
|
||||
User::Settings.create!(
|
||||
user: new_user,
|
||||
bundle_email_frequency: old_settings.bundle_email_frequency,
|
||||
timezone_name: old_settings.timezone_name
|
||||
)
|
||||
end
|
||||
|
||||
mapping[:users][old_user.id] = new_user.id
|
||||
|
||||
Reference in New Issue
Block a user