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 %> + +
+
+
+
+
+

<%= t("reset_password.subtitle") %>

+
+
+ <%= 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 %> + +
+
+
+
+
+

<%= t("forgot_password.subtitle") %>

+
+
+ <%= 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 %> +
+
+ <%= link_to t("modal.login.forgot_password"), new_password_reset_path %> +
+
+ <% 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