Skip to content

Commit edf6b53

Browse files
committed
Introduce an "owner" role, and prevent it from being administered
to prevent the owner from being demoted or kicked out. ref: https://app.fizzy.do/5986089/cards/3213
1 parent 156a23f commit edf6b53

File tree

20 files changed

+167
-37
lines changed

20 files changed

+167
-37
lines changed

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Fizzy uses **URL path-based multi-tenancy**:
6868

6969
**Passwordless magic link authentication**:
7070
- Global `Identity` (email-based) can have `Users` in multiple Accounts
71-
- Users belong to an Account and have roles: admin, member, system
71+
- Users belong to an Account and have roles: owner, admin, member, system
7272
- Sessions managed via signed cookies
7373
- Board-level access control via `Access` records
7474

@@ -84,7 +84,7 @@ Fizzy uses **URL path-based multi-tenancy**:
8484

8585
**User** → Account membership
8686
- Belongs to Account and Identity
87-
- Has role (admin/member/system)
87+
- Has role (owner/admin/member/system)
8888
- Board access via explicit `Access` records
8989

9090
**Board** → Primary organizational unit

app/helpers/users_helper.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module UsersHelper
2+
def role_display_name(user)
3+
case user.role
4+
when "admin" then "Administrator"
5+
else user.role.titleize
6+
end
7+
end
8+
end

app/models/account.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ class Account < ApplicationRecord
1717
validates :name, presence: true
1818

1919
class << self
20-
def create_with_admin_user(account:, owner:)
20+
def create_with_owner(account:, owner:)
2121
create!(**account).tap do |account|
2222
account.users.create!(role: :system, name: "System")
23-
account.users.create!(**owner.reverse_merge(role: "admin"))
23+
account.users.create!(**owner.reverse_merge(role: "owner"))
2424
end
2525
end
2626
end

app/models/account/seedeable.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ module Account::Seedeable
22
extend ActiveSupport::Concern
33

44
def setup_customer_template
5-
Account::Seeder.new(self, users.active.where(role: :admin).first).seed
5+
Account::Seeder.new(self, users.admin.first).seed
66
end
77
end

app/models/signup.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def handle_account_creation_error(error)
5454
end
5555

5656
def create_account
57-
@account = Account.create_with_admin_user(
57+
@account = Account.create_with_owner(
5858
account: {
5959
external_account_id: @tenant,
6060
name: generate_account_name
@@ -64,7 +64,7 @@ def create_account
6464
identity: identity
6565
}
6666
)
67-
@user = @account.users.find_by!(role: :admin)
67+
@user = @account.users.find_by!(role: :owner)
6868
@account.setup_customer_template
6969
end
7070

app/models/user/role.rb

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,24 @@ module User::Role
22
extend ActiveSupport::Concern
33

44
included do
5-
enum :role, %i[ admin member system ].index_by(&:itself), scopes: false
5+
enum :role, %i[ owner admin member system ].index_by(&:itself), scopes: false
66

7-
scope :member, -> { where(role: :member) }
8-
scope :active, -> { where(active: true, role: %i[ admin member ]) }
7+
scope :owner, -> { where(active: true, role: :owner) }
8+
scope :admin, -> { where(active: true, role: %i[ owner admin ]) }
9+
scope :member, -> { where(active: true, role: :member) }
10+
scope :active, -> { where(active: true, role: %i[ owner admin member ]) }
11+
12+
def admin?
13+
super || owner?
14+
end
915
end
1016

1117
def can_change?(other)
12-
admin? || other == self
18+
(admin? && !other.owner?) || other == self
1319
end
1420

1521
def can_administer?(other)
16-
admin? && other != self
22+
admin? && !other.owner? && other != self
1723
end
1824

1925
def can_administer_board?(board)

app/views/account/settings/_user.html.erb

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
<hr class="separator--horizontal flex-item-grow" style="--border-color: var(--color-ink-medium); --border-style: dashed" aria-hidden="true">
1111

1212
<%= form_with model: user, url: user_role_path(user), data: { controller: "form" }, method: :patch do | form | %>
13-
<label class="btn btn--circle" for="<%= dom_id(user, :role) %>" arial-label="Role: <%= user.admin? ? "Administrator" : "Member" %>">
14-
<%= icon_tag "crown" %>
15-
<span class="for-screen-reader">Role: <%= user.admin? ? "Administrator" : "Member" %></span>
16-
<%= form.check_box :role, { data: { action: "form#submit" }, disabled: !Current.user.can_administer?(user), hidden: true, id: dom_id(user, :role) }, "admin", "member" %>
17-
</label>
13+
<span title="<%= role_display_name(user) %>">
14+
<label class="btn btn--circle" for="<%= dom_id(user, :role) %>" aria-label="Role: <%= role_display_name(user) %>">
15+
<%= icon_tag "crown" %>
16+
<span class="for-screen-reader">Role: <%= role_display_name(user) %></span>
17+
<%= form.check_box :role, { data: { action: "form#submit" }, disabled: !Current.user.can_administer?(user), checked: user.admin?, hidden: true, id: dom_id(user, :role) }, "admin", "member" %>
18+
</label>
19+
</span>
1820
<% end %>
1921

2022
<%# FIXME: Move this Current.user check to a stimulus controller that just checks for admin? or the like we so we can cache user list %>

app/views/admin/stats/show.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070

7171
<ul class="margin-block-half">
7272
<% @top_accounts.each do |account| %>
73-
<% admin_user = account.users.find { |u| u.role == "admin" } %>
73+
<% admin_user = account.users.owner.first %>
7474
<li class="flex align-start gap-half margin-block-start-half">
7575
<div
7676
class="flex-item-grow min-width overflow-ellipsis"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class PromoteFirstAdminToOwner < ActiveRecord::Migration[8.2]
2+
def up
3+
Account.find_each do |account|
4+
next if account.users.exists?(role: :owner)
5+
6+
first_admin = account.users.where(role: :admin).order(:created_at).first
7+
first_admin&.update!(role: :owner)
8+
end
9+
end
10+
11+
def down
12+
User.where(role: :owner).update_all(role: :admin)
13+
end
14+
end

db/schema.rb

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)