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)