From 087cac3426c5902f45a16314e6fb3c893be21277 Mon Sep 17 00:00:00 2001
From: Jake Hash <1508098+jhash@users.noreply.github.com>
Date: Fri, 25 Jul 2025 17:07:36 -0400
Subject: [PATCH 1/2] Add superadmin role and task to promote users
---
app/controllers/home_controller.rb | 2 +-
app/controllers/sessions_controller.rb | 8 +--
app/models/identity.rb | 2 +-
app/models/role.rb | 12 ++++
app/models/user.rb | 24 ++++++--
app/models/user_role.rb | 6 ++
app/views/layouts/application.html.erb | 3 +
db/cable_schema.rb | 6 +-
db/cache_schema.rb | 6 +-
db/migrate/20250725193121_create_users.rb | 2 +-
.../20250725195922_create_identities.rb | 4 +-
db/migrate/20250725205833_create_roles.rb | 10 ++++
.../20250725205853_create_user_roles.rb | 12 ++++
db/schema.rb | 27 +++++++--
lib/tasks/superadmin.rake | 59 +++++++++++++++++++
15 files changed, 159 insertions(+), 24 deletions(-)
create mode 100644 app/models/role.rb
create mode 100644 app/models/user_role.rb
create mode 100644 db/migrate/20250725205833_create_roles.rb
create mode 100644 db/migrate/20250725205853_create_user_roles.rb
create mode 100644 lib/tasks/superadmin.rake
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 9ec3adc..95f2992 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -1,4 +1,4 @@
class HomeController < ApplicationController
def index
end
-end
\ No newline at end of file
+end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index dd0efea..ba7e2ba 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -1,11 +1,11 @@
class SessionsController < ApplicationController
def create
auth = request.env["omniauth.auth"]
-
+
if logged_in?
# User is already logged in, so link the new provider to their account
identity = current_user.identities.find_or_create_by(provider: auth.provider, uid: auth.uid)
-
+
if identity.persisted?
redirect_to root_path, notice: "#{auth.provider.humanize} account connected successfully!"
else
@@ -14,7 +14,7 @@ def create
else
# User is not logged in, so sign them in
user = User.from_omniauth(auth)
-
+
if user
session[:user_id] = user.id
redirect_to root_path, notice: "Signed in successfully"
@@ -32,4 +32,4 @@ def destroy
def failure
redirect_to root_path, alert: "Authentication failed: #{params[:message]}"
end
-end
\ No newline at end of file
+end
diff --git a/app/models/identity.rb b/app/models/identity.rb
index ea45bee..ec78e32 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -1,6 +1,6 @@
class Identity < ApplicationRecord
belongs_to :user
-
+
validates :provider, presence: true
validates :uid, presence: true
validates :uid, uniqueness: { scope: :provider }
diff --git a/app/models/role.rb b/app/models/role.rb
new file mode 100644
index 0000000..04b4d0d
--- /dev/null
+++ b/app/models/role.rb
@@ -0,0 +1,12 @@
+class Role < ApplicationRecord
+ has_many :user_roles, dependent: :destroy
+ has_many :users, through: :user_roles
+
+ validates :name, presence: true, uniqueness: true
+
+ SUPERADMIN = "superadmin"
+
+ def self.superadmin
+ find_or_create_by(name: SUPERADMIN)
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 13acd42..31ec11f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,6 +1,8 @@
class User < ApplicationRecord
has_many :identities, dependent: :destroy
-
+ has_many :user_roles, dependent: :destroy
+ has_many :roles, through: :user_roles
+
validates :email, presence: true, uniqueness: true
def self.from_omniauth(auth)
@@ -12,23 +14,35 @@ def self.from_omniauth(auth)
end
id.user = user
end
-
+
# Update user name if it's better than what we have
if identity.user.name.blank? || identity.user.name == "User"
identity.user.update(name: auth.info.name || auth.info.nickname || identity.user.name)
end
-
+
identity.user
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error "OAuth authentication failed: #{e.message}"
nil
end
-
+
def connected_providers
identities.pluck(:provider)
end
-
+
def provider_connected?(provider)
identities.exists?(provider: provider)
end
+
+ def superadmin?
+ roles.exists?(name: Role::SUPERADMIN)
+ end
+
+ def make_superadmin!
+ roles << Role.superadmin unless superadmin?
+ end
+
+ def remove_superadmin!
+ roles.delete(Role.superadmin) if superadmin?
+ end
end
diff --git a/app/models/user_role.rb b/app/models/user_role.rb
new file mode 100644
index 0000000..ce18119
--- /dev/null
+++ b/app/models/user_role.rb
@@ -0,0 +1,6 @@
+class UserRole < ApplicationRecord
+ belongs_to :user
+ belongs_to :role
+
+ validates :user_id, uniqueness: { scope: :role_id }
+end
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 03a38a8..fac910a 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -30,6 +30,9 @@
<% if logged_in? %>
Welcome, <%= current_user.name %>
+ <% if current_user.superadmin? %>
+ <%= link_to "Admin", "#", class: "bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded text-sm" %>
+ <% end %>
<%= button_to "Sign Out", logout_path, method: :delete,
class: "bg-red-600 hover:bg-red-700 px-4 py-2 rounded text-sm",
data: { turbo: false } %>
diff --git a/db/cable_schema.rb b/db/cable_schema.rb
index 2366660..eef9db1 100644
--- a/db/cable_schema.rb
+++ b/db/cable_schema.rb
@@ -4,8 +4,8 @@
t.binary "payload", limit: 536870912, null: false
t.datetime "created_at", null: false
t.integer "channel_hash", limit: 8, null: false
- t.index ["channel"], name: "index_solid_cable_messages_on_channel"
- t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash"
- t.index ["created_at"], name: "index_solid_cable_messages_on_created_at"
+ t.index [ "channel" ], name: "index_solid_cable_messages_on_channel"
+ t.index [ "channel_hash" ], name: "index_solid_cable_messages_on_channel_hash"
+ t.index [ "created_at" ], name: "index_solid_cable_messages_on_created_at"
end
end
diff --git a/db/cache_schema.rb b/db/cache_schema.rb
index 6005a29..3ac1f3f 100644
--- a/db/cache_schema.rb
+++ b/db/cache_schema.rb
@@ -7,8 +7,8 @@
t.datetime "created_at", null: false
t.integer "key_hash", limit: 8, null: false
t.integer "byte_size", limit: 4, null: false
- t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size"
- t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size"
- t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true
+ t.index [ "byte_size" ], name: "index_solid_cache_entries_on_byte_size"
+ t.index [ "key_hash", "byte_size" ], name: "index_solid_cache_entries_on_key_hash_and_byte_size"
+ t.index [ "key_hash" ], name: "index_solid_cache_entries_on_key_hash", unique: true
end
end
diff --git a/db/migrate/20250725193121_create_users.rb b/db/migrate/20250725193121_create_users.rb
index dd4ff8f..a027c42 100644
--- a/db/migrate/20250725193121_create_users.rb
+++ b/db/migrate/20250725193121_create_users.rb
@@ -9,6 +9,6 @@ def change
t.timestamps
end
add_index :users, :email, unique: true
- add_index :users, [:provider, :uid], unique: true
+ add_index :users, [ :provider, :uid ], unique: true
end
end
diff --git a/db/migrate/20250725195922_create_identities.rb b/db/migrate/20250725195922_create_identities.rb
index 1e1538d..b84fe79 100644
--- a/db/migrate/20250725195922_create_identities.rb
+++ b/db/migrate/20250725195922_create_identities.rb
@@ -7,7 +7,7 @@ def change
t.timestamps
end
-
- add_index :identities, [:provider, :uid], unique: true
+
+ add_index :identities, [ :provider, :uid ], unique: true
end
end
diff --git a/db/migrate/20250725205833_create_roles.rb b/db/migrate/20250725205833_create_roles.rb
new file mode 100644
index 0000000..204309e
--- /dev/null
+++ b/db/migrate/20250725205833_create_roles.rb
@@ -0,0 +1,10 @@
+class CreateRoles < ActiveRecord::Migration[8.0]
+ def change
+ create_table :roles do |t|
+ t.string :name, null: false
+
+ t.timestamps
+ end
+ add_index :roles, :name, unique: true
+ end
+end
diff --git a/db/migrate/20250725205853_create_user_roles.rb b/db/migrate/20250725205853_create_user_roles.rb
new file mode 100644
index 0000000..17d20a0
--- /dev/null
+++ b/db/migrate/20250725205853_create_user_roles.rb
@@ -0,0 +1,12 @@
+class CreateUserRoles < ActiveRecord::Migration[8.0]
+ def change
+ create_table :user_roles do |t|
+ t.references :user, null: false, foreign_key: true
+ t.references :role, null: false, foreign_key: true
+
+ t.timestamps
+ end
+
+ add_index :user_roles, [ :user_id, :role_id ], unique: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 0caf9d3..b7ece33 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,15 +10,32 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.0].define(version: 2025_07_25_195936) do
+ActiveRecord::Schema[8.0].define(version: 2025_07_25_205853) do
create_table "identities", force: :cascade do |t|
t.integer "user_id", null: false
t.string "provider"
t.string "uid"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.index ["provider", "uid"], name: "index_identities_on_provider_and_uid", unique: true
- t.index ["user_id"], name: "index_identities_on_user_id"
+ t.index [ "provider", "uid" ], name: "index_identities_on_provider_and_uid", unique: true
+ t.index [ "user_id" ], name: "index_identities_on_user_id"
+ end
+
+ create_table "roles", force: :cascade do |t|
+ t.string "name", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index [ "name" ], name: "index_roles_on_name", unique: true
+ end
+
+ create_table "user_roles", force: :cascade do |t|
+ t.integer "user_id", null: false
+ t.integer "role_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index [ "role_id" ], name: "index_user_roles_on_role_id"
+ t.index [ "user_id", "role_id" ], name: "index_user_roles_on_user_id_and_role_id", unique: true
+ t.index [ "user_id" ], name: "index_user_roles_on_user_id"
end
create_table "users", force: :cascade do |t|
@@ -26,8 +43,10 @@
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.index ["email"], name: "index_users_on_email", unique: true
+ t.index [ "email" ], name: "index_users_on_email", unique: true
end
add_foreign_key "identities", "users"
+ add_foreign_key "user_roles", "roles"
+ add_foreign_key "user_roles", "users"
end
diff --git a/lib/tasks/superadmin.rake b/lib/tasks/superadmin.rake
new file mode 100644
index 0000000..94cb5c2
--- /dev/null
+++ b/lib/tasks/superadmin.rake
@@ -0,0 +1,59 @@
+namespace :superadmin do
+ desc "Make a user a superadmin by email"
+ task :make, [ :email ] => :environment do |t, args|
+ unless args[:email]
+ puts "Please provide an email: rake superadmin:make[user@example.com]"
+ exit 1
+ end
+
+ user = User.find_by(email: args[:email])
+
+ if user.nil?
+ puts "User with email '#{args[:email]}' not found"
+ exit 1
+ end
+
+ if user.superadmin?
+ puts "User '#{args[:email]}' is already a superadmin"
+ else
+ user.make_superadmin!
+ puts "User '#{args[:email]}' has been made a superadmin"
+ end
+ end
+
+ desc "Remove superadmin role from a user by email"
+ task :unmake, [ :email ] => :environment do |t, args|
+ unless args[:email]
+ puts "Please provide an email: rake superadmin:unmake[user@example.com]"
+ exit 1
+ end
+
+ user = User.find_by(email: args[:email])
+
+ if user.nil?
+ puts "User with email '#{args[:email]}' not found"
+ exit 1
+ end
+
+ if user.superadmin?
+ user.remove_superadmin!
+ puts "Superadmin role removed from user '#{args[:email]}'"
+ else
+ puts "User '#{args[:email]}' is not a superadmin"
+ end
+ end
+
+ desc "List all superadmins"
+ task list: :environment do
+ superadmins = User.joins(:roles).where(roles: { name: Role::SUPERADMIN })
+
+ if superadmins.any?
+ puts "Superadmins:"
+ superadmins.each do |user|
+ puts " - #{user.email} (#{user.name})"
+ end
+ else
+ puts "No superadmins found"
+ end
+ end
+end
From dd62c88ba801aa72d2c50665be6e67af6f2e235c Mon Sep 17 00:00:00 2001
From: Jake Hash <1508098+jhash@users.noreply.github.com>
Date: Fri, 25 Jul 2025 17:09:12 -0400
Subject: [PATCH 2/2] Update brakeman
---
Gemfile.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index c280fc6..001835d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -95,7 +95,7 @@ GEM
bindex (0.8.1)
bootsnap (1.18.6)
msgpack (~> 1.2)
- brakeman (7.0.2)
+ brakeman (7.1.0)
racc
builder (3.3.0)
capybara (3.40.0)