Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,24 @@ alt="IMAGE ALT TEXT HERE" width="560" height="315" border="10" /></a>
### Database Credentials
- username: root
- password: expertiza


# Password Reset Testing Guide

## Overview
The deployment does not allow creating new users. To facilitate testing, we have manually added a test email into the database. Follow the steps below to test the password reset functionality.

## Steps for Testing

1) Go to [http://152.7.177.227:3000/login](http://152.7.177.227:3000/login) and click on 'forget password' button.

2) Input the email address: **[email protected]** and click on request password.

3) Open another tab and log into Gmail using the following credentials:

- **Email:** `[email protected]`
- **Pass:** `Test@1234`

4) After logging in, you should be able to see the inbox and there should be an email from Expertiza Mailer(check spam folder if you don't see the email). Open the email and click on the link to reset the password.

5) Type in the new password and reset it. Then head back to [http://152.7.177.227:3000/login](http://152.7.177.227:3000/login) and try logging in with the email and the new password that you set up.
44 changes: 44 additions & 0 deletions app/controllers/api/v1/passwords_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
class Api::V1::PasswordsController < ApplicationController
before_action :find_user_by_email, only: [:create]
before_action :find_user_by_token, only: [:update]
skip_before_action :authenticate_request!, only: [:create, :update]

# User requests a password reset
def create
if @user
@user.generate_password_reset_token!
UserMailer.send_password_reset_email(@user).deliver_later
render json: { message: "If the email exists, a reset link has been sent." }, status: :ok
else
render json: { error: "No account is associated with the e-mail address: #{params[:email]}. Please try again." }, status: :not_found
end
end

# Update password
def update
if [email protected]_reset_valid?
render json: { error: "The token has expired or is invalid." }, status: :unprocessable_entity
elsif @user.update(password_params)
@user.clear_password_reset_token!
render json: { message: "Password successfully updated." }, status: :ok
else
render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
end
end

private

def find_user_by_email
@user = User.find_by(email: params[:email])
end

def find_user_by_token
@user = User.find_by(reset_password_token: params[:token])
return render json: { error: "Invalid or expired token." }, status: :unprocessable_entity unless @user&.password_reset_valid?
end

def password_params
params.require(:user).permit(:password, :password_confirmation)
end
end

9 changes: 9 additions & 0 deletions app/mailers/user_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class UserMailer < ApplicationMailer
default from: "[email protected]"

def send_password_reset_email(user)
@user = user
@reset_url = "http://localhost:3000/password_edit/check_reset_url?token=#{@user.reset_password_token}"
mail(to: @user.email, subject: 'Expertiza password reset')
end
end
20 changes: 20 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,24 @@ def set_defaults
self.etc_icons_on_homepage ||= true
end


validates :reset_password_token, uniqueness: true, allow_nil: true

# Method to generate reset password token
def generate_password_reset_token!
self.reset_password_token = SecureRandom.urlsafe_base64
self.reset_password_sent_at = Time.zone.now
save!
end

# Method to clear the reset token after a successful password reset
def clear_password_reset_token!
update(reset_password_token: nil, reset_password_sent_at: nil)
end

# Method to check if the password reset token is valid (within 24 hours)
def password_reset_valid?
(reset_password_sent_at + 24.hours) > Time.zone.now
end

end
14 changes: 14 additions & 0 deletions app/views/user_mailer/send_password_reset_email.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<html>
<head>
<title>Expertiza password reset</title>
</head>

<body>
<p>Hi ,</p>
<p>Reset your password, and we'll get you on your way.</p>
<p>To change your password, click or paste the following link into your browser:</p>
<p><a href="<%= @reset_url %>"><%= @reset_url %></a></p>
<p>The link will expire in 24 hours, so be sure to use it right away.</p>
</body>
</html>

14 changes: 14 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@
# Highlight code that triggered database queries in logs.
config.active_record.verbose_query_logs = true

config.action_mailer.delivery_method = :smtp
config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = true
config.action_mailer.smtp_settings = {
address: 'smtp.gmail.com',
port: 587,
domain: 'localhost',
user_name: '[email protected]',
password: 'xdgmnehqevkevkqy', # This password should come from a .env file
authentication: 'plain',
enable_starttls_auto: true
}
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true

Expand Down
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
post '/login', to: 'authentication#login'
namespace :api do
namespace :v1 do
resources :password_resets, only: [:create, :update], controller: "passwords", param: :token
resources :institutions
resources :roles do
collection do
Expand Down Expand Up @@ -120,6 +121,8 @@
delete '/:id', to: 'participants#destroy'
end
end

resources :password_resets, only: [:create, :update]
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddPasswordResetFieldsToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :reset_password_token, :string
add_column :users, :reset_password_sent_at, :datetime
end
end
90 changes: 90 additions & 0 deletions spec/controllers/api/v1/passwords_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
require 'rails_helper'

RSpec.describe Api::V1::PasswordsController, type: :controller do
let(:user) { create(:user) }
let(:valid_password_params) { { user: { password: 'newpassword123', password_confirmation: 'newpassword123' } } }
let(:invalid_password_params) { { user: { password: 'short', password_confirmation: 'short' } } }

describe 'PasswordsController' do
describe '#create' do
context 'when the email exists' do
before do
allow(UserMailer).to receive_message_chain(:send_password_reset_email, :deliver_later)
post :create, params: { email: user.email }
end

it 'generates a password reset token' do
user.reload
expect(user.reset_password_token).to be_present
end

it 'sends a password reset email' do
expect(UserMailer).to have_received(:send_password_reset_email).with(user)
end

it 'returns a success message' do
expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body)['message']).to eq("If the email exists, a reset link has been sent.")
end
end

context 'when the email does not exist' do
before do
post :create, params: { email: '[email protected]' }
end

it 'returns an error message' do
expect(response).to have_http_status(:not_found)
expect(JSON.parse(response.body)['error']).to eq("No account is associated with the e-mail address: [email protected]. Please try again.")
end
end
end

describe '#update' do
context 'when the token is valid' do
before do
user.generate_password_reset_token!
put :update, params: { token: user.reset_password_token }.merge(valid_password_params)
end

it 'updates the password' do
user.reload
expect(user.authenticate('newpassword123')).to be_truthy
end

it 'clears the password reset token' do
user.reload
expect(user.reset_password_token).to be_nil
end

it 'returns a success message' do
expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body)['message']).to eq("Password successfully updated.")
end
end

context 'when the token is invalid or expired' do
before do
put :update, params: { token: 'invalidtoken' }.merge(valid_password_params)
end

it 'returns an error message' do
expect(response).to have_http_status(:unprocessable_entity)
expect(JSON.parse(response.body)['error']).to eq("Invalid or expired token.")
end
end

context 'when the password is invalid' do
before do
user.generate_password_reset_token!
put :update, params: { token: user.reset_password_token }.merge(invalid_password_params)
end

it 'returns validation errors' do
expect(response).to have_http_status(:unprocessable_entity)
expect(JSON.parse(response.body)['errors']).to include("Password is too short (minimum is 8 characters)")
end
end
end
end
end
14 changes: 14 additions & 0 deletions spec/factories/users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# spec/factories/users.rb
FactoryBot.factories.clear
FactoryBot.define do
factory :user do
email { Faker::Internet.email }
password { 'password123' }
password_confirmation { 'password123' }
name { Faker::Name.first_name }
full_name { Faker::Name.name }
association :role
reset_password_sent_at { nil }
end
end

Loading