diff --git a/Gemfile b/Gemfile
index 7a46838f..3640634d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -71,3 +71,4 @@ end
gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby]
gem "devise", "~> 4.9"
+gem "devise_invitable", "~> 2.0"
diff --git a/Gemfile.lock b/Gemfile.lock
index 2dc41061..3184a32a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -130,6 +130,9 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
+ devise_invitable (2.0.9)
+ actionmailer (>= 5.0)
+ devise (>= 4.6)
drb (2.2.1)
erubi (1.13.0)
execjs (2.9.1)
@@ -411,6 +414,7 @@ DEPENDENCIES
comfortable_mexican_sofa!
cssbundling-rails
devise (~> 4.9)
+ devise_invitable (~> 2.0)
htmlentities
image_processing (~> 1.13)
jbuilder (~> 2.12)
diff --git a/app/models/admin.rb b/app/models/admin.rb
index 5d130f24..883e5d7e 100644
--- a/app/models/admin.rb
+++ b/app/models/admin.rb
@@ -3,6 +3,6 @@
class Admin < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :registerable, :timeoutable, and :omniauthable
- devise :database_authenticatable,
+ devise :invitable, :database_authenticatable,
:recoverable, :rememberable, :trackable, :validatable
end
diff --git a/app/views/admin/admins/index.html.erb b/app/views/admin/admins/index.html.erb
index 75a88393..700a2d8d 100644
--- a/app/views/admin/admins/index.html.erb
+++ b/app/views/admin/admins/index.html.erb
@@ -1,5 +1,5 @@
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 3480f0a0..68ba40da 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -134,6 +134,55 @@
# Send a notification email when the user's password is changed.
# config.send_password_change_notification = false
+ # ==> Configuration for :invitable
+ # The period the generated invitation token is valid.
+ # After this period, the invited resource won't be able to accept the invitation.
+ # When invite_for is 0 (the default), the invitation won't expire.
+ # config.invite_for = 2.weeks
+
+ # Number of invitations users can send.
+ # - If invitation_limit is nil, there is no limit for invitations, users can
+ # send unlimited invitations, invitation_limit column is not used.
+ # - If invitation_limit is 0, users can't send invitations by default.
+ # - If invitation_limit n > 0, users can send n invitations.
+ # You can change invitation_limit column for some users so they can send more
+ # or less invitations, even with global invitation_limit = 0
+ # Default: nil
+ # config.invitation_limit = 5
+
+ # The key to be used to check existing users when sending an invitation
+ # and the regexp used to test it when validate_on_invite is not set.
+ # config.invite_key = { email: /\A[^@]+@[^@]+\z/ }
+ # config.invite_key = { email: /\A[^@]+@[^@]+\z/, username: nil }
+
+ # Ensure that invited record is valid.
+ # The invitation won't be sent if this check fails.
+ # Default: false
+ # config.validate_on_invite = true
+
+ # Resend invitation if user with invited status is invited again
+ # Default: true
+ # config.resend_invitation = false
+
+ # The class name of the inviting model. If this is nil,
+ # the #invited_by association is declared to be polymorphic.
+ # Default: nil
+ # config.invited_by_class_name = 'User'
+
+ # The foreign key to the inviting model (if invited_by_class_name is set)
+ # Default: :invited_by_id
+ # config.invited_by_foreign_key = :invited_by_id
+
+ # The column name used for counter_cache column. If this is nil,
+ # the #invited_by association is declared without counter_cache.
+ # Default: nil
+ # config.invited_by_counter_cache = :invitations_count
+
+ # Auto-login after the user accepts the invite. If this is false,
+ # the user will need to manually log in after accepting the invite.
+ # Default: true
+ # config.allow_insecure_sign_in_after_accept = false
+
# ==> Configuration for :confirmable
# A period that the user is allowed to access the website even without
# confirming their account. For instance, if set to 2.days, the user will be
diff --git a/config/locales/devise_invitable.en.yml b/config/locales/devise_invitable.en.yml
new file mode 100644
index 00000000..f6bfee40
--- /dev/null
+++ b/config/locales/devise_invitable.en.yml
@@ -0,0 +1,31 @@
+en:
+ devise:
+ failure:
+ invited: "You have a pending invitation, accept it to finish creating your account."
+ invitations:
+ send_instructions: "An invitation email has been sent to %{email}."
+ invitation_token_invalid: "The invitation token provided is not valid!"
+ updated: "Your password was set successfully. You are now signed in."
+ updated_not_active: "Your password was set successfully."
+ no_invitations_remaining: "No invitations remaining"
+ invitation_removed: "Your invitation was removed."
+ new:
+ header: "Send invitation"
+ submit_button: "Send an invitation"
+ edit:
+ header: "Set your password"
+ submit_button: "Set my password"
+ mailer:
+ invitation_instructions:
+ subject: "Invitation instructions"
+ hello: "Hello %{email}"
+ someone_invited_you: "Someone has invited you to %{url}, you can accept it through the link below."
+ accept: "Accept invitation"
+ accept_until: "This invitation will be due in %{due_date}."
+ ignore: "If you don't want to accept the invitation, please ignore this email. Your account won't be created until you access the link above and set your password."
+ time:
+ formats:
+ devise:
+ mailer:
+ invitation_instructions:
+ accept_until_format: "%B %d, %Y %I:%M %p"
diff --git a/db/migrate/20240830185023_devise_invitable_add_to_admins.rb b/db/migrate/20240830185023_devise_invitable_add_to_admins.rb
new file mode 100644
index 00000000..df0ab5b5
--- /dev/null
+++ b/db/migrate/20240830185023_devise_invitable_add_to_admins.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class DeviseInvitableAddToAdmins < ActiveRecord::Migration[7.1]
+ def up
+ change_table :admins, bulk: true do |t|
+ t.string :invitation_token
+ t.datetime :invitation_created_at
+ t.datetime :invitation_sent_at
+ t.datetime :invitation_accepted_at
+ t.integer :invitation_limit
+ t.references :invited_by, polymorphic: true
+ t.integer :invitations_count, default: 0
+ t.index :invitation_token, unique: true # for invitable
+ t.index :invited_by_id
+ end
+ end
+
+ def down
+ change_table :admins, bulk: true do |t|
+ t.remove_references :invited_by, polymorphic: true
+ t.remove :invitations_count, :invitation_limit, :invitation_sent_at, :invitation_accepted_at, :invitation_token, :invitation_created_at
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f97b007b..5f80c4e7 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2024_08_14_221626) do
+ActiveRecord::Schema[7.1].define(version: 2024_08_30_185023) do
create_table "active_storage_attachments", charset: "utf8mb3", collation: "utf8mb3_general_ci", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
@@ -52,7 +52,18 @@
t.string "last_sign_in_ip"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.string "invitation_token"
+ t.datetime "invitation_created_at"
+ t.datetime "invitation_sent_at"
+ t.datetime "invitation_accepted_at"
+ t.integer "invitation_limit"
+ t.string "invited_by_type"
+ t.bigint "invited_by_id"
+ t.integer "invitations_count", default: 0
t.index ["email"], name: "index_admins_on_email", unique: true
+ t.index ["invitation_token"], name: "index_admins_on_invitation_token", unique: true
+ t.index ["invited_by_id"], name: "index_admins_on_invited_by_id"
+ t.index ["invited_by_type", "invited_by_id"], name: "index_admins_on_invited_by"
t.index ["reset_password_token"], name: "index_admins_on_reset_password_token", unique: true
end
diff --git a/test/integration/admin_invitation_test.rb b/test/integration/admin_invitation_test.rb
new file mode 100644
index 00000000..c193e38d
--- /dev/null
+++ b/test/integration/admin_invitation_test.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class AdminInvitationTest < ActionDispatch::IntegrationTest
+ setup do
+ @admin = admins(:admin)
+ end
+
+ test "not able to invite other admins when logged out" do
+ get new_admin_invitation_path
+
+ assert_redirected_to new_admin_session_path
+ end
+
+ test "able to invite other admins when admin" do
+ sign_in @admin
+
+ get new_admin_invitation_path
+
+ assert_response :success
+ end
+
+ # TODO: Add more tests here for the basic functionality:
+ # test "create an admin invitation" do
+ # end
+
+ # test "accepting an admin invitation" do
+ # end
+end