diff --git a/Gemfile b/Gemfile
index 3ecd69d4a..2c31d8655 100755
--- a/Gemfile
+++ b/Gemfile
@@ -15,6 +15,7 @@ gem 'turbolinks', '~> 5'
gem 'colorize', require: false
gem 'cookies_eu'
gem 'devise', '>= 4.6.2'
+gem 'devise_invitable', '~> 2.0'
gem 'font-awesome-sass', '>= 4.4.0'
gem 'gabba'
gem 'intercom-rails'
diff --git a/Gemfile.lock b/Gemfile.lock
index a502feb5a..e357eae77 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -93,6 +93,9 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
+ devise_invitable (2.0.3)
+ actionmailer (>= 5.0)
+ devise (>= 4.6)
docile (1.3.2)
erubi (1.10.0)
execjs (2.7.0)
@@ -305,6 +308,7 @@ DEPENDENCIES
colorize
cookies_eu
devise (>= 4.6.2)
+ devise_invitable (~> 2.0)
font-awesome-sass (>= 4.4.0)
foreman
gabba
diff --git a/README.md b/README.md
index 7929fea09..5c3c4becf 100644
--- a/README.md
+++ b/README.md
@@ -389,6 +389,20 @@ which will install/upgrade the Node module, and then save that dependency to `pa
Then check in the updated `package.json` and `yarn.lock` files.
+## I'd like to use a new Ruby Gem
+
+Typically you would simply do:
+
+```
+bin/docker r bundle add foobar
+```
+
+which will install the new Gem, and then save that dependency to `Gemfile`.
+
+Then check in the updated `package.json` and `yarn.lock` files. For good measure
+run the `bin/setup_docker`.
+
+
## I'd like to test SSL
There's a directory `.ssl` that contains they key and cert files used for SSL. This is a self signed generated certificate for use in development ONLY!
@@ -451,11 +465,11 @@ heroku restart -a quepid-staging
## Seed Data
-The following accounts are created through the seeds. The all follow the following format:
+The following accounts are created through the seeds. They all follow the following format:
```
email: quepid+[type]@o19s.com
-password: quepid+[type]
+password: password
```
where type is one of the following:
@@ -498,7 +512,6 @@ To comply with GDPR, and be a good citizen, the hosted version of Quepid asks if
EMAIL_MARKETING_MODE=true # Enables a checkbox on user signup to consent to emails
```
-
# Thank You's
Quepid would not be possible without the contributions from many individuals and organizations.
diff --git a/UPGRADING_RAILS_NOTES.md b/UPGRADING_RAILS_NOTES.md
index c3e423707..171b45989 100644
--- a/UPGRADING_RAILS_NOTES.md
+++ b/UPGRADING_RAILS_NOTES.md
@@ -14,8 +14,33 @@ Todos:
* DONE (Changed my mind, I used it to reduce some extra sql joins etc) rip out extra dev analystics stuff
* DONE (no issue!) export of general and detail from js doesn't work.
* DONE Look at session in home_controller, do we use it???
+* DONE, (password blank works fine). Chase down why :password="" is needed when inviting a user.
+* Deal with the format of the emails! Make them quepid qlassy.
+* DONE Deal with environment variable for disabling forms.
+https://github.com/gonzalo-bulnes/simple_token_authentication
+https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html
+https://github.com/lynndylanhurley/devise_token_auth
+
+https://www.codementor.io/@gowiem/deviseinvitable-rails-api-9wzmbisus
+
+
+User.invite!(email: 'joe@example.com', name: 'Joe', password:'password')
+
+user = User.invite!(email: 'joe3@example.com', name: 'joe3', password:'password') do |u|
+ u.skip_invitation = true
+end
+
+user = User.invite!({ email: 'joe8@example.com', name: 'Joe8', password:'password' }, current_user)
+User.invite!({ email: 'new_user@example.com' }, current_user)
+
+
+User.accept_invitation!(invitation_token: params[:invitation_token], password: 'ad97nwj3o2', name: 'John Doe')
+User.accept_invitation!(invitation_token: '9ngHVdcWyvSNrg54a8yj', password: 'ad97nwj3o2', name: 'John Doe')
+
+
+user = User.invite!({ email: 'joe9@example.com' }, current_user)
KEY! http://railsdiff.org/4.2.11/5.2.4.4
diff --git a/app/assets/javascripts/components/add_member/add_member.html b/app/assets/javascripts/components/add_member/add_member.html
index 476880998..f2307eb2a 100644
--- a/app/assets/javascripts/components/add_member/add_member.html
+++ b/app/assets/javascripts/components/add_member/add_member.html
@@ -1,28 +1,40 @@
- Start typing an email to add user as a member to this team.
- Once you select a user, click on "Add" to add them as a member.
+ Start typing an email address to add an existing user to this team.
+ If the email address doesn't match anyone, you will be able to invite them to join Quepid and this team.
diff --git a/app/controllers/api/api_controller.rb b/app/controllers/api/api_controller.rb
index 32a41ea61..652742cd4 100644
--- a/app/controllers/api/api_controller.rb
+++ b/app/controllers/api/api_controller.rb
@@ -27,6 +27,10 @@ def test
render json: { message: 'Success!' }, status: :ok
end
+ def signup_enabled?
+ Rails.application.config.signup_enabled
+ end
+
protected
def set_default_response_format
diff --git a/app/controllers/api/v1/signups_controller.rb b/app/controllers/api/v1/signups_controller.rb
index a2ca0184d..2dd50c1a0 100644
--- a/app/controllers/api/v1/signups_controller.rb
+++ b/app/controllers/api/v1/signups_controller.rb
@@ -8,7 +8,6 @@ class SignupsController < Api::ApiController
def create
@user = User.new user_params
- @user.agreed_time = Time.zone.now
if @user.save
Analytics::Tracker.track_signup_event @user
diff --git a/app/controllers/api/v1/team_members_controller.rb b/app/controllers/api/v1/team_members_controller.rb
index 0289f4b0d..e60d3da75 100644
--- a/app/controllers/api/v1/team_members_controller.rb
+++ b/app/controllers/api/v1/team_members_controller.rb
@@ -3,8 +3,8 @@
module Api
module V1
class TeamMembersController < Api::ApiController
- before_action :set_team, only: [ :index, :create, :destroy ]
- before_action :check_team, only: [ :index, :create, :destroy ]
+ before_action :set_team, only: [ :index, :create, :destroy, :invite ]
+ before_action :check_team, only: [ :index, :create, :destroy, :invite ]
def index
@members = @team.members
@@ -29,6 +29,24 @@ def create
end
end
+ def invite
+ unless signup_enabled?
+ render json: { error: 'Signups are disabled!' }, status: :not_found
+ return
+ end
+
+ @member = User.invite!({ email: params[:id], password: '' }, current_user)
+
+ @team.members << @member unless @team.members.exists?(@member.id)
+
+ if @team.save
+ Analytics::Tracker.track_member_added_to_team_event current_user, @team, @member
+ respond_with @member
+ else
+ render json: @member.errors, status: :bad_request
+ end
+ end
+
def destroy
member = @team.members.where('email = ? OR id = ?', params[:id].to_s.downcase, params[:id])
diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb
index a0b9abf75..7b038331b 100644
--- a/app/controllers/api/v1/users_controller.rb
+++ b/app/controllers/api/v1/users_controller.rb
@@ -9,7 +9,7 @@ def index
@users = []
if params[:prefix]
prefix = params[:prefix].downcase
- @users = User.where('email LIKE :prefix OR name LIKE :prefix', prefix: "#{prefix}%").limit(8)
+ @users = User.where('`email` LIKE :prefix', prefix: "#{prefix}%").limit(8)
end
respond_with @users
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 97b668cff..3167f50b3 100755
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -23,4 +23,8 @@ class ApplicationController < ActionController::Base
def ssl_enabled?
'true' == ENV['FORCE_SSL']
end
+
+ def signup_enabled?
+ Rails.application.config.signup_enabled
+ end
end
diff --git a/app/controllers/concerns/authentication/current_team_manager.rb b/app/controllers/concerns/authentication/current_team_manager.rb
index 64aa582c2..10b7d47bb 100644
--- a/app/controllers/concerns/authentication/current_team_manager.rb
+++ b/app/controllers/concerns/authentication/current_team_manager.rb
@@ -7,9 +7,10 @@ module CurrentTeamManager
private
def set_team
- @team = current_user.teams_im_in.where(id: params[:team_id])
- .preload(:members)
- .first
+ # @team = current_user.teams_im_in.where(id: params[:team_id])
+ # .preload(:members)
+ # .first
+ @team = current_user.teams_im_in.where(id: params[:team_id]).first
end
def check_team
diff --git a/app/controllers/users/invitations_controller.rb b/app/controllers/users/invitations_controller.rb
new file mode 100644
index 000000000..e13edd74f
--- /dev/null
+++ b/app/controllers/users/invitations_controller.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Users
+ class InvitationsController < Devise::InvitationsController
+ force_ssl if: :ssl_enabled?
+ skip_before_action :require_login, only: [ :edit, :update ]
+
+ def update
+ unless signup_enabled?
+ flash.now[:error] = 'Signups are disabled.'
+ redirect_to secure_path and return
+ end
+
+ super
+
+ @user.agreed_time = Time.zone.now
+ session[:current_user_id] = @user.id
+ Analytics::Tracker.track_signup_event @user
+ end
+
+ # rubocop:disable Lint/UselessMethodDefinition
+ def edit
+ super
+ end
+ # rubocop:enable Lint/UselessMethodDefinition
+
+ private
+
+ def update_resource_params
+ params.require(:user).permit(
+ :name,
+ :email,
+ :invitation_token,
+ :password,
+ :password_confirmation,
+ :agreed,
+ :email_marketing
+ )
+ end
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 2e9d3b939..6107a118f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -68,28 +68,38 @@ class User < ApplicationRecord
dependent: :destroy
# Validations
+
+ # https://davidcel.is/posts/stop-validating-email-addresses-with-regex/
validates :email,
presence: true,
- uniqueness: true
+ uniqueness: true,
+ format: { with: URI::MailTo::EMAIL_REGEXP }
validates :password,
presence: true
validates_with ::DefaultScorerExistsValidator
+ validates :agreed,
+ acceptance: { message: 'You must agree to the terms and conditions.' },
+ if: :terms_and_conditions?
+
+ def terms_and_conditions?
+ Rails.application.config.terms_and_conditions_url.length.positive?
+ end
+
# Modules
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
- # devise :database_authenticatable, :registerable,
+ # devise :invitable, :database_authenticatable, :registerable,
# :recoverable, :rememberable, :trackable, :validatable
- devise :recoverable, reset_password_keys: [ :email ]
+ devise :invitable, :recoverable, reset_password_keys: [ :email ]
# Callbacks
before_save :encrypt_password
+ before_save :check_agreed_time
before_create :set_defaults
- # after_create :add_default_case
-
# Devise hacks since we only use the recoverable module
attr_accessor :password_confirmation
@@ -131,7 +141,7 @@ def cases_involved_with
# Returns all the teams that the user is both owner of and involved in!
def teams_im_in
- UserTeamFinder.new(self).call
+ UserTeamFinder.new(self)
end
def locked?
@@ -148,6 +158,10 @@ def unlock
self.locked_at = nil
end
+ def after_database_authentication
+ # required by devise_invitable
+ end
+
private
def set_defaults
@@ -156,15 +170,17 @@ def set_defaults
self.num_logins = 0 if num_logins.nil?
self.default_scorer = Scorer.system_default_scorer if self.default_scorer.nil?
# rubocop:enable Style/RedundantSelf
-
- # this is necessary because it will rollback
- # the creation/update of the user otherwise
- true
end
def encrypt_password
self[:password] = BCrypt::Password.create(password) if password.present? && password_changed?
+ end
+
+ def check_agreed_time
+ return unless terms_and_conditions?
+
+ return unless agreed && agreed_time.nil?
- true
+ self[:agreed_time] = Time.zone.now
end
end
diff --git a/app/services/user_team_finder.rb b/app/services/user_team_finder.rb
index e94ea8ae9..98a677776 100644
--- a/app/services/user_team_finder.rb
+++ b/app/services/user_team_finder.rb
@@ -5,10 +5,18 @@ class UserTeamFinder
def initialize user
@user = user
+ @teams = Team.references(:users).for_user(@user)
end
- def call
- Team.references(:users)
- .for_user(@user)
+ def method_missing method_name, *arguments, &block
+ if @teams.respond_to? method_name
+ @teams.send(method_name, *arguments, &block)
+ else
+ super
+ end
+ end
+
+ def respond_to_missing? method_name, include_private = false
+ @teams.respond_to?(method_name) || super
end
end
diff --git a/app/views/api/v1/snapshots/_snapshot.json.jbuilder b/app/views/api/v1/snapshots/_snapshot.json.jbuilder
index b66d6e358..a4d58b90c 100644
--- a/app/views/api/v1/snapshots/_snapshot.json.jbuilder
+++ b/app/views/api/v1/snapshots/_snapshot.json.jbuilder
@@ -18,6 +18,7 @@ end
if with_docs
json.queries do
- json.array! snapshot.snapshot_queries.collect(&:query), partial: 'api/v1/queries/query', as: :query
+ # filter out deleted queries from the snapshot via the .compact method.
+ json.array! snapshot.snapshot_queries.collect(&:query).compact, partial: 'api/v1/queries/query', as: :query
end
end
diff --git a/app/views/api/v1/team_members/_member.json.jbuilder b/app/views/api/v1/team_members/_member.json.jbuilder
index e241466d2..224664f8a 100644
--- a/app/views/api/v1/team_members/_member.json.jbuilder
+++ b/app/views/api/v1/team_members/_member.json.jbuilder
@@ -4,3 +4,4 @@ json.id member.id
json.display_name member.display_name
json.email member.email
json.avatar_url member.avatar_url(:big)
+json.pending_invite member.created_by_invite? && !member.invitation_accepted?
diff --git a/app/views/api/v1/team_members/invite.json.jbuilder b/app/views/api/v1/team_members/invite.json.jbuilder
new file mode 100644
index 000000000..63757cff1
--- /dev/null
+++ b/app/views/api/v1/team_members/invite.json.jbuilder
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+json.partial! 'member', member: @member
diff --git a/app/views/api/v1/teams/_team.json.jbuilder b/app/views/api/v1/teams/_team.json.jbuilder
index 2d80df685..b6934ba8f 100644
--- a/app/views/api/v1/teams/_team.json.jbuilder
+++ b/app/views/api/v1/teams/_team.json.jbuilder
@@ -16,6 +16,7 @@ json.members team.members do |member|
json.display_name member.display_name
json.email member.email
json.avatar_url member.avatar_url(:big)
+ json.pending_invite member.created_by_invite? && !member.invitation_accepted?
end
if load_cases
diff --git a/app/views/devise/mailer/invitation_instructions.html.erb b/app/views/devise/mailer/invitation_instructions.html.erb
new file mode 100644
index 000000000..8ea6556ec
--- /dev/null
+++ b/app/views/devise/mailer/invitation_instructions.html.erb
@@ -0,0 +1,7 @@
+
Hello <%= @resource.email %>
+
+<%= User.find_by(id: @resource.invited_by_id).name %> has invited you to join their team on Quepid at <%= root_url %>, you can accept it through the link below.
+
+
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.
diff --git a/app/views/devise/mailer/invitation_instructions.text.erb b/app/views/devise/mailer/invitation_instructions.text.erb
new file mode 100644
index 000000000..d554c2265
--- /dev/null
+++ b/app/views/devise/mailer/invitation_instructions.text.erb
@@ -0,0 +1,7 @@
+Hello <%= @resource.email %>
+
+<%= User.find_by(id: @resource.invited_by_id).name %> has invited you to join their team on Quepid at <%= root_url %>, you can accept it through the link below.
+
+<%= accept_invitation_url(@resource, invitation_token: @token) %>
+
+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.
diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb
index 406e73f5e..6f1fa85c2 100644
--- a/app/views/devise/mailer/reset_password_instructions.html.erb
+++ b/app/views/devise/mailer/reset_password_instructions.html.erb
@@ -1,6 +1,6 @@
Hello <%= @resource.display_name %>!
-
Someone has requested a link to change your password.
+
Someone has requested a link to change your password on Quepid.
You can do this by using the one time link below:
diff --git a/app/views/users/invitations/edit.html.erb b/app/views/users/invitations/edit.html.erb
new file mode 100644
index 000000000..7783afcad
--- /dev/null
+++ b/app/views/users/invitations/edit.html.erb
@@ -0,0 +1,75 @@
+