diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb
new file mode 100644
index 00000000..e3a8af34
--- /dev/null
+++ b/app/controllers/password_resets_controller.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+#
+# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+#
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with BigBlueButton; if not, see .
+
+class PasswordResetsController < ApplicationController
+ before_action :disable_password_reset, unless: -> { Rails.configuration.enable_email_verification }
+ before_action :find_user, only: [:edit, :update]
+ before_action :valid_user, only: [:edit, :update]
+ before_action :check_expiration, only: [:edit, :update]
+
+ def index
+ end
+
+ def create
+ @user = User.find_by(email: params[:password_reset][:email].downcase)
+ if @user
+ @user.create_reset_digest
+ @user.send_password_reset_email(request.base_url)
+ redirect_to root_url, notice: I18n.t("email_sent")
+ else
+ redirect_to new_password_reset_path, notice: I18n.t("no_user_email_exists")
+ end
+ rescue => e
+ logger.error "Error in email delivery: #{e}"
+ redirect_to root_path, notice: I18n.t(params[:message], default: I18n.t("delivery_error"))
+ end
+
+ def edit
+ end
+
+ def update
+ if params[:user][:password].empty?
+ flash.now[:notice] = I18n.t("password_empty_notice")
+ render 'edit'
+ elsif params[:user][:password] != params[:user][:password_confirmation]
+ flash.now[:notice] = I18n.t("password_different_notice")
+ render 'edit'
+ elsif current_user.update_attributes(user_params)
+ redirect_to root_path, notice: I18n.t("password_reset_success")
+ else
+ render 'edit'
+ end
+ end
+
+ private
+
+ def find_user
+ @user = User.find_by(email: params[:email])
+ end
+
+ def current_user
+ @user
+ end
+
+ def user_params
+ params.require(:user).permit(:password, :password_confirmation)
+ end
+
+ # Checks expiration of reset token.
+ def check_expiration
+ if current_user.password_reset_expired?
+ redirect_to new_password_reset_url, notice: I18n.t("expired_reset_token")
+ end
+ end
+
+ # Confirms a valid user.
+ def valid_user
+ unless current_user&.email_verified && current_user.authenticated?(:reset, params[:id])
+ redirect_to root_url
+ end
+ end
+
+ def disable_password_reset
+ redirect_to '/404'
+ end
+end
diff --git a/app/helpers/password_resets_helper.rb b/app/helpers/password_resets_helper.rb
new file mode 100644
index 00000000..bfc3d834
--- /dev/null
+++ b/app/helpers/password_resets_helper.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+#
+# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+#
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with BigBlueButton; if not, see .
+
+module PasswordResetsHelper
+end
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index 31f230c6..abef46de 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -24,4 +24,10 @@ class UserMailer < ApplicationMailer
@url = url
mail(to: @user.email, subject: t('landing.welcome'))
end
+
+ def password_reset(user, url)
+ @user = user
+ @url = url
+ mail to: user.email, subject: t('reset_password.subtitle')
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 72b7a680..77863712 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -17,6 +17,7 @@
# with BigBlueButton; if not, see .
class User < ApplicationRecord
+ attr_accessor :reset_token
after_create :initialize_main_room
before_save { email.try(:downcase!) }
@@ -93,6 +94,30 @@ class User < ApplicationRecord
end
end
+ # Sets the password reset attributes.
+ def create_reset_digest
+ self.reset_token = User.new_token
+ update_attribute(:reset_digest, User.digest(reset_token))
+ update_attribute(:reset_sent_at, Time.zone.now)
+ end
+
+ # Sends password reset email.
+ def send_password_reset_email(url)
+ UserMailer.password_reset(self, url).deliver_now
+ end
+
+ # Returns true if the given token matches the digest.
+ def authenticated?(attribute, token)
+ digest = send("#{attribute}_digest")
+ return false if digest.nil?
+ BCrypt::Password.new(digest).is_password?(token)
+ end
+
+ # Return true if password reset link expires
+ def password_reset_expired?
+ reset_sent_at < 2.hours.ago
+ end
+
# Retrives a list of all a users rooms that are not the main room, sorted by last session date.
def secondary_rooms
secondary = (rooms - [main_room])
@@ -119,6 +144,16 @@ class User < ApplicationRecord
provider == "greenlight"
end
+ def self.digest(string)
+ cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
+ BCrypt::Password.create(string, cost: cost)
+ end
+
+ # Returns a random token.
+ def self.new_token
+ SecureRandom.urlsafe_base64
+ end
+
private
# Destory a users rooms when they are removed.
diff --git a/app/views/password_resets/edit.html.erb b/app/views/password_resets/edit.html.erb
new file mode 100644
index 00000000..929db9e2
--- /dev/null
+++ b/app/views/password_resets/edit.html.erb
@@ -0,0 +1,50 @@
+<%
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+# You should have received a copy of the GNU Lesser General Public License along
+# with BigBlueButton; if not, see .
+%>
+
+<% unless flash.empty? %>
+ <%= render "shared/error_banner" do %>
+ <% flash.each do |key, value| %>
+ <%= content_tag :div, value, class: "flash #{key} d-inline" %>
+ <% end %>
+ <% end %>
+<% end %>
+
+
+
+
+
+
+
+ <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
+
+ <%= hidden_field_tag :email, @user.email %>
+
+ <%= f.label t('reset_password.password'), class: "form-label" %>
+ <%= f.password_field :password, class: 'form-control' %>
+
+
+ <%= f.label t('reset_password.confirm'), class: "form-label" %>
+ <%= f.password_field :password_confirmation, class: 'form-control' %>
+
+
+ <%= f.submit t('reset_password.update'), class: "btn btn-primary" %>
+ <% end %>
+
+
+
+
+
\ No newline at end of file
diff --git a/app/views/password_resets/new.html.erb b/app/views/password_resets/new.html.erb
new file mode 100644
index 00000000..6493277a
--- /dev/null
+++ b/app/views/password_resets/new.html.erb
@@ -0,0 +1,43 @@
+<%
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+# You should have received a copy of the GNU Lesser General Public License along
+# with BigBlueButton; if not, see .
+%>
+
+<% unless flash.empty? %>
+ <%= render "shared/error_banner" do %>
+ <% flash.each do |key, value| %>
+ <%= content_tag :div, value, class: "flash #{key} d-inline" %>
+ <% end %>
+ <% end %>
+<% end %>
+
+
+
+
+
+
+
+ <%= form_for(:password_reset, url: password_resets_path) do |f| %>
+ <%= f.label t("forgot_password.email"), class: "form-label" %>
+ <%= f.email_field :email, class: "form-control" %>
+
+
+ <%= f.submit t("forgot_password.submit"), class: "btn btn-primary" %>
+ <% end %>
+
+
+
+
+
diff --git a/app/views/shared/modals/_login_modal.html.erb b/app/views/shared/modals/_login_modal.html.erb
index a0fa176d..c5e4c3a3 100644
--- a/app/views/shared/modals/_login_modal.html.erb
+++ b/app/views/shared/modals/_login_modal.html.erb
@@ -55,6 +55,13 @@
<%= f.password_field :password, class: "form-control", placeholder: t("password"), value: "" %>
+ <% if Rails.configuration.enable_email_verification %>
+
+ <% end %>
diff --git a/app/views/user_mailer/password_reset.html.erb b/app/views/user_mailer/password_reset.html.erb
new file mode 100644
index 00000000..c5ab0da0
--- /dev/null
+++ b/app/views/user_mailer/password_reset.html.erb
@@ -0,0 +1,32 @@
+<%
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+#
+# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+#
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with BigBlueButton; if not, see .
+%>
+
+Password reset
+
+Please click the link below to reset your password:
+
+<%= link_to "Reset password", edit_password_reset_url(@user.reset_token,
+ email: @user.email,
+ host: @url) %>
+
+This link will expire in two hours.
+
+
+If you did not request your password to be reset, please ignore this email and
+your password will not be changed.
+
\ No newline at end of file
diff --git a/app/views/user_mailer/password_reset.text.erb b/app/views/user_mailer/password_reset.text.erb
new file mode 100644
index 00000000..044674bf
--- /dev/null
+++ b/app/views/user_mailer/password_reset.text.erb
@@ -0,0 +1,26 @@
+<%
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+#
+# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+#
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with BigBlueButton; if not, see .
+%>
+
+Please click the link below to reset your password:
+
+<%= edit_password_reset_url(@user.reset_token, email: @user.email, host: @url) %>
+
+This link will expire in two hours.
+
+If you did not request your password to be reset, please ignore this email and
+your password will not be changed.
\ No newline at end of file
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 6df77ff9..933a7b82 100755
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -29,6 +29,7 @@ en:
delivery_error: An error occured during email delivery. Please contact an administrator!
docs: Documentation
email: Email
+ email_sent: Email Sent!
enter_your_name: Enter your name!
errors:
internal:
@@ -49,6 +50,7 @@ en:
unprocessable:
message: Oops! Request is unprocessable.
help: Unforunately this isn't a valid request.
+ expired_reset_token: Password reset link has expired!
features:
title: Features
rooms: Personalized Rooms
@@ -57,6 +59,10 @@ en:
authentication: User Authentication
footer:
powered_by: Powered by %{href}.
+ forgot_password:
+ subtitle: Forgot Password
+ email: Email
+ submit: Submit
go_back: Go back
greenlight: Greenlight
header:
@@ -109,13 +115,18 @@ en:
login:
or: or
with: Sign in with %{provider}
+ forgot_password: Forgot Password?
rename_recording:
rename_room:
name_placeholder: Enter a new room name...
name_update_success: Room name successfully changed!
+ no_user_email_exists: There is no existing user with the email specified. Please make sure you typed it correctly.
omniauth_error: An error occured while authenticating with omniauth. Please try again or contact an administrator!
password: Password
+ password_empty_notice: Password cannot be empty.
+ password_reset_success: Password has been reset.
+ password_different_notice: Password Confirmation does not match.
provider:
google: Google
microsoft_office365: Office 365
@@ -136,6 +147,11 @@ en:
public: Public
unlisted: Unlisted
rename: Rename
+ reset_password:
+ subtitle: Reset Password
+ password: New Password
+ confirm: New Password Confirmation
+ update: Update Password
room:
invited: You have been invited to join
invite_participants: Invite Participants
diff --git a/config/routes.rb b/config/routes.rb
index 69b429fd..ad63dc2c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -31,6 +31,9 @@ Rails.application.routes.draw do
# Redirect to terms page
match '/terms', to: 'users#terms', via: [:get, :post]
+ # Password reset resources.
+ resources :password_resets, only: [:new, :create, :edit, :update]
+
# User resources.
scope '/u' do
# Verification Routes
diff --git a/db/migrate/20181217142710_add_reset_to_users.rb b/db/migrate/20181217142710_add_reset_to_users.rb
new file mode 100644
index 00000000..29a47c86
--- /dev/null
+++ b/db/migrate/20181217142710_add_reset_to_users.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+#
+# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+#
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with BigBlueButton; if not, see .
+
+class AddResetToUsers < ActiveRecord::Migration[5.0]
+ def change
+ add_column :users, :reset_digest, :string
+ add_column :users, :reset_sent_at, :datetime
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 434d6c02..2626d041 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.define(version: 20181113174230) do
+ActiveRecord::Schema.define(version: 20181217142710) do
create_table "rooms", force: :cascade do |t|
t.integer "user_id"
@@ -40,10 +40,13 @@ ActiveRecord::Schema.define(version: 20181113174230) do
t.string "image"
t.string "password_digest"
t.boolean "accepted_terms", default: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
t.boolean "email_verified", default: false
t.string "language", default: "default"
+ t.string "role", default: "moderator"
+ t.string "reset_digest"
+ t.datetime "reset_sent_at"
t.index ["password_digest"], name: "index_users_on_password_digest", unique: true
t.index ["room_id"], name: "index_users_on_room_id"
end
diff --git a/spec/controllers/password_resets_controller_spec.rb b/spec/controllers/password_resets_controller_spec.rb
new file mode 100644
index 00000000..1fdd6b1f
--- /dev/null
+++ b/spec/controllers/password_resets_controller_spec.rb
@@ -0,0 +1,138 @@
+# frozen_string_literal: true
+
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+#
+# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+#
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with BigBlueButton; if not, see .
+
+require "rails_helper"
+
+def random_valid_user_params
+ pass = Faker::Internet.password(8)
+ {
+ user: {
+ name: Faker::Name.first_name,
+ email: Faker::Internet.email,
+ password: pass,
+ password_confirmation: pass,
+ accepted_terms: true,
+ email_verified: true,
+ },
+ }
+end
+
+describe PasswordResetsController, type: :controller do
+ describe "POST #create" do
+ context "allow mail notifications" do
+ before { allow(Rails.configuration).to receive(:enable_email_verification).and_return(true) }
+
+ it "redirects to root url if email is sent" do
+ user = create(:user)
+
+ params = {
+ password_reset: {
+ email: user.email,
+ },
+ }
+
+ post :create, params: params
+ expect(response).to redirect_to(root_path)
+ end
+
+ it "reloads the page if no email exists in the database" do
+ params = {
+ password_reset: {
+ email: nil,
+ },
+ }
+
+ post :create, params: params
+ expect(response).to redirect_to(new_password_reset_path)
+ end
+ end
+
+ context "does not allow mail notifications" do
+ before { allow(Rails.configuration).to receive(:enable_email_verification).and_return(false) }
+
+ it "renders a 404 page upon if email notifications are disabled" do
+ get :create
+ expect(response).to redirect_to("/404")
+ end
+ end
+ end
+
+ describe "PATCH #update" do
+ before { allow(Rails.configuration).to receive(:enable_email_verification).and_return(true) }
+
+ context "valid user" do
+ it "reloads page with notice if password is empty" do
+ token = "reset_token"
+
+ controller.stub(:valid_user).and_return(nil)
+ controller.stub(:check_expiration).and_return(nil)
+
+ params = {
+ id: token,
+ user: {
+ password: nil,
+ },
+ }
+
+ patch :update, params: params
+ expect(response).to render_template(:edit)
+ end
+
+ it "reloads page with notice if password is confirmation doesn't match" do
+ token = "reset_token"
+
+ controller.stub(:valid_user).and_return(nil)
+ controller.stub(:check_expiration).and_return(nil)
+
+ params = {
+ id: token,
+ user: {
+ password: :password,
+ password_confirmation: nil,
+ },
+ }
+
+ patch :update, params: params
+ expect(response).to render_template(:edit)
+ end
+
+ it "updates attributes if the password update is a success" do
+ user = create(:user)
+ token = "reset_token"
+
+ cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
+ user.reset_digest = BCrypt::Password.create(token, cost: cost)
+
+ controller.stub(:valid_user).and_return(nil)
+ controller.stub(:check_expiration).and_return(nil)
+ controller.stub(:current_user).and_return(user)
+
+ params = {
+ id: token,
+ user: {
+ password: :password,
+ password_confirmation: :password,
+ },
+ }
+
+ patch :update, params: params
+ expect(response).to redirect_to(root_path)
+ end
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 33fffc04..93703ace 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -109,4 +109,21 @@ describe User, type: :model do
expect(user.name_chunk).to eq("exa")
end
end
+
+ context 'password reset' do
+ it 'creates token and respective reset digest' do
+ user = create(:user)
+
+ reset_digest_success = user.create_reset_digest
+ expect(reset_digest_success).to eq(true)
+ end
+
+ it 'verifies if password reset link expired' do
+ user = create(:user)
+ user.create_reset_digest
+
+ expired = user.password_reset_expired?
+ expect(expired).to be_in([true, false])
+ end
+ end
end