Skip to content

Commit

Permalink
add devise invitable and a invite and accept invite mutation
Browse files Browse the repository at this point in the history
  • Loading branch information
simonfranzen committed Sep 20, 2020
1 parent cbc37ae commit 21d0e48
Show file tree
Hide file tree
Showing 15 changed files with 298 additions and 34 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ gem 'pg'

gem 'bcrypt', '~> 3.1.7' # Use ActiveModel has_secure_password
gem 'devise' # Use devise as authentication module
gem 'devise_invitable', '~> 2.0.0' # Used to invite users. Allows setting passwords by invited user
gem 'graphql', '~> 1.11.4' # GraphQL as API
gem 'graphql-auth', git: 'https://github.com/simonfranzen/graphql-auth.git', branch: 'rails6'
gem 'graphql-errors' # GrapqhQL error handling
Expand Down
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ GEM
warden (~> 1.2.3)
devise-i18n (1.9.2)
devise (>= 4.7.1)
devise_invitable (2.0.2)
actionmailer (>= 5.0)
devise (>= 4.6)
diff-lcs (1.4.4)
docile (1.3.2)
dotenv (2.7.6)
Expand Down Expand Up @@ -340,6 +343,7 @@ DEPENDENCIES
database_cleaner (~> 1.6)
devise
devise-i18n
devise_invitable (~> 2.0.0)
dotenv-rails
factory_bot_rails
faker (~> 1.8)
Expand Down
14 changes: 14 additions & 0 deletions app/controllers/auth/invitations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

module Auth
# Custom passwords controller
class InvitationsController < Devise::InvitationsController

# GET /resource/invitation/accept?invitation_token=abcdef
# redirect user to front end to finish invitation
def edit
redirect_to "http://#{ENV['CLIENT_URL']}/users/invitation/accept?invitation_token=#{params[:invitation_token]}"
end

end
end
20 changes: 20 additions & 0 deletions app/graphql/mutations/users/accept_invite.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module Mutations
module Users
# Accepts an invitation for a user
class AcceptInvite < Mutations::BaseMutation
description 'Accepts an invitation for a user'
argument :invitation_token, String, required: true
argument :attributes, Types::Users::UserInputType, required: true
payload_type Boolean

def resolve(invitation_token:, attributes:)
user = User.accept_invitation!(attributes.to_h.merge(invitation_token: invitation_token))
raise ActiveRecord::RecordInvalid, user unless user.errors.empty?

true
end
end
end
end
23 changes: 23 additions & 0 deletions app/graphql/mutations/users/invite_user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module Mutations
module Users
# Invites an user to your account.
class InviteUser < Mutations::BaseMutation
description 'Invites an user to your account.'
argument :attributes, Types::Users::UserInputType, required: true
payload_type Types::Users::UserType

def resolve(attributes:)
# create a dummy user object to check ability against create
user = ::User.new(attributes.to_h.merge(company_id: context[:current_user].company_id))
current_ability.authorize! :create, user

user = User.invite!(user.attributes, context[:current_user])
raise ActiveRecord::RecordInvalid, user unless user.errors.empty?

user
end
end
end
end
2 changes: 2 additions & 0 deletions app/graphql/types/mutation_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class MutationType < Types::BaseObject
field :update_user, mutation: Mutations::Users::UpdateUser
field :update_user_role, mutation: Mutations::Users::UpdateUserRole
field :delete_user, mutation: Mutations::Users::DeleteUser
field :invite_user, mutation: Mutations::Users::InviteUser
field :accept_invite, mutation: Mutations::Users::AcceptInvite

# Company
field :update_company, mutation: Mutations::Companies::UpdateCompany
Expand Down
31 changes: 16 additions & 15 deletions app/models/companies/company.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: companies
#
# id :uuid not null, primary key
# name :string
# slug :string
# users_count :integer
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_companies_on_slug (slug) UNIQUE
#
module Companies
# == Schema Information
#
# Table name: companies
#
# id :uuid not null, primary key
# name :string
# slug :string
# users_count :integer
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_companies_on_slug (slug) UNIQUE
#
# A company to scope a bunch of users
class Company < ApplicationRecord
extend FriendlyId

Expand Down
28 changes: 21 additions & 7 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,15 @@
# encrypted_password :string default(""), not null
# failed_attempts :integer default(0), not null
# first_name :string default(""), not null
# invitation_accepted_at :datetime
# invitation_created_at :datetime
# invitation_limit :integer
# invitation_sent_at :datetime
# invitation_token :string
# invitations_count :integer default(0)
# invited_by_type :string
# last_name :string default(""), not null
# last_seen_at :datetime
# last_sign_in_at :datetime
# last_sign_in_ip :inet
# locked_at :datetime
Expand All @@ -29,26 +37,32 @@
# created_at :datetime not null
# updated_at :datetime not null
# company_id :uuid
# invited_by_id :bigint
#
# Indexes
#
# index_users_on_confirmation_token (confirmation_token) UNIQUE
# index_users_on_email (email) UNIQUE
# index_users_on_refresh_token (refresh_token) UNIQUE
# index_users_on_reset_password_token (reset_password_token) UNIQUE
# index_users_on_unlock_token (unlock_token) UNIQUE
# index_users_on_confirmation_token (confirmation_token) UNIQUE
# index_users_on_email (email) UNIQUE
# index_users_on_invitation_token (invitation_token) UNIQUE
# index_users_on_invitations_count (invitations_count)
# index_users_on_invited_by_id (invited_by_id)
# index_users_on_invited_by_type_and_invited_by_id (invited_by_type,invited_by_id)
# index_users_on_refresh_token (refresh_token) UNIQUE
# index_users_on_reset_password_token (reset_password_token) UNIQUE
# index_users_on_unlock_token (unlock_token) UNIQUE
#
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :timeoutable, :trackable and :omniauthable
devise :database_authenticatable,
devise :invitable, :database_authenticatable,
:registerable,
:recoverable,
:confirmable,
:devise,
:validatable,
:lockable,
:trackable
:trackable,
:invitable

# add new roles to the end
enum role: { user: 0, admin: 1, superadmin: 2 }
Expand Down
49 changes: 49 additions & 0 deletions config/initializers/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,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 = 0

# 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
Expand Down
31 changes: 31 additions & 0 deletions config/locales/devise_invitable.en.yml
Original file line number Diff line number Diff line change
@@ -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"
3 changes: 2 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
devise_for :users,
controllers: {
confirmations: 'auth/confirmations',
passwords: 'auth/passwords'
passwords: 'auth/passwords',
invitations: 'auth/invitations'
},
skip: :registrations # skip registration route

Expand Down
23 changes: 23 additions & 0 deletions db/migrate/20200920102035_devise_invitable_add_to_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class DeviseInvitableAddToUsers < ActiveRecord::Migration[6.0]
def up
change_table :users 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 :invitations_count
t.index :invitation_token, unique: true # for invitable
t.index :invited_by_id
end
end

def down
change_table :users 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
57 changes: 56 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2020_09_12_153858) do
ActiveRecord::Schema.define(version: 2020_09_20_102035) do

# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
Expand Down Expand Up @@ -46,6 +46,23 @@
t.index ["slug"], name: "index_companies_on_slug", unique: true
end

create_table "conversation_memberships", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.uuid "user_id"
t.uuid "conversation_id"
t.uuid "last_read_message_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["user_id", "conversation_id"], name: "index_user_id_on_conversation_id", unique: true
end

create_table "conversations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "name"
t.uuid "owner_id"
t.uuid "company_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end

create_table "friendly_id_slugs", force: :cascade do |t|
t.string "slug", null: false
t.integer "sluggable_id", null: false
Expand All @@ -58,6 +75,31 @@
t.index ["sluggable_type"], name: "index_friendly_id_slugs_on_sluggable_type"
end

create_table "messages", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.uuid "conversation_id"
t.uuid "user_id"
t.text "body"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end

create_table "notifications", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "notification_type"
t.uuid "initiator_id"
t.uuid "receiver_id"
t.boolean "recent", default: true
t.boolean "is_read", default: false
t.boolean "is_delivered", default: false
t.uuid "resource_id"
t.string "resource_type"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["initiator_id"], name: "index_notifications_on_initiator_id"
t.index ["notification_type"], name: "index_notifications_on_notification_type"
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
t.index ["resource_id", "resource_type"], name: "index_notifications_on_resource_id_and_resource_type"
end

create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "first_name", default: "", null: false
t.string "last_name", default: "", null: false
Expand All @@ -83,8 +125,21 @@
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.uuid "company_id"
t.datetime "last_seen_at"
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 ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["invitation_token"], name: "index_users_on_invitation_token", unique: true
t.index ["invitations_count"], name: "index_users_on_invitations_count"
t.index ["invited_by_id"], name: "index_users_on_invited_by_id"
t.index ["invited_by_type", "invited_by_id"], name: "index_users_on_invited_by_type_and_invited_by_id"
t.index ["refresh_token"], name: "index_users_on_refresh_token", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
Expand Down
Loading

0 comments on commit 21d0e48

Please sign in to comment.