From ec7a79c8bb902bde1c5f6d09da2922dc5f986cd7 Mon Sep 17 00:00:00 2001 From: Nuwan Date: Sat, 28 Sep 2024 22:11:48 +0530 Subject: [PATCH] forgot password feature --- jam-ui/cypress/e2e/auth/forgot-password.cy.js | 54 +++++++++++++++++++ jam-ui/cypress/e2e/auth/login.cy.js | 13 ++++- .../src/components/auth/ConfirmMailContent.js | 36 ++++++++----- .../src/components/auth/ForgetPasswordForm.js | 18 +++---- jam-ui/src/components/auth/LoginForm.js | 19 ++++--- .../src/components/auth/PasswordResetForm.js | 38 ++++++++++--- .../components/auth/basic/ForgetPassword.js | 7 +-- jam-ui/src/components/auth/basic/Login.js | 41 +++++++------- .../components/auth/basic/PasswordReset.js | 17 +++--- jam-ui/src/i18n/locales/en/auth.json | 33 +++++++++++- jam-ui/src/layouts/JKAuthBasicLayout.js | 9 ++-- .../controllers/api_sessions_controller.rb | 14 ++++- web/config/routes.rb | 2 +- 13 files changed, 225 insertions(+), 76 deletions(-) create mode 100644 jam-ui/cypress/e2e/auth/forgot-password.cy.js diff --git a/jam-ui/cypress/e2e/auth/forgot-password.cy.js b/jam-ui/cypress/e2e/auth/forgot-password.cy.js new file mode 100644 index 000000000..4997a8870 --- /dev/null +++ b/jam-ui/cypress/e2e/auth/forgot-password.cy.js @@ -0,0 +1,54 @@ +/// + +import makeFakeUser from '../../factories/user'; + +describe('forgot password', () => { + beforeEach(() => { + const currentUser = makeFakeUser({ + email: 'sam@example.com' + }); + cy.clearCookie('remeber_token'); + }); + + it('redirects to forgot password page', () => { + cy.visit('/'); + cy.url().should('include', '/authentication/basic/login'); + cy.get('a') + .contains('Forgot password?') + .click(); + cy.url().should('include', '/authentication/basic/forget-password'); + cy.get('h5').contains('Forgot Your Password'); + }); + + describe('validate forgot password form', () => { + beforeEach(() => { + cy.visit('/authentication/basic/forget-password'); + cy.get('[data-testid=email]').clear(); + cy.get('[data-testid=submit]').should('be.disabled'); + }); + + //invalid email format + it('invalid email format', () => { + cy.get('[data-testid=email]').type('invalid-email-format@example'); + cy.get('[data-testid=submit]').click(); + cy.url().should('not.include', /\/authentication\/basic\/confirm-mail?\S+/); + }); + + //valid email format but non-existing + it('valid email format but non-existing', () => { + cy.get('[data-testid=email]').type('valid-email-format@example.com'); + cy.get('[data-testid=submit]').click(); + cy.url().should('not.include', /\/authentication\/basic\/confirm-mail?\S+/); + }); + + //valid and existing email + it('valid and existing email', () => { + cy.get('[data-testid=email]').type('nuwan@jamkazam.com'); + cy.get('[data-testid=submit]').click(); + cy.wait(3000); + cy.contains('Please check your email!'); + cy.url().should('match', /\S+authentication\/basic\/confirm-mail?\S+/); + cy.contains('An email has been sent to nuwan@jamkazam.com.') + }); + }); +}); diff --git a/jam-ui/cypress/e2e/auth/login.cy.js b/jam-ui/cypress/e2e/auth/login.cy.js index 4b6fac0c1..7cf1808f1 100644 --- a/jam-ui/cypress/e2e/auth/login.cy.js +++ b/jam-ui/cypress/e2e/auth/login.cy.js @@ -30,9 +30,9 @@ function submitLogin(){ describe('Unauthenticated users redirect to login page', () => { it('redirects to login page', () => { cy.clearCookie('remeber_token') - cy.visit('/friends') + cy.visit('/') cy.url().should('include', '/authentication/basic/login') - cy.contains('Sign in') + cy.contains('Sign In') }) }) @@ -81,4 +81,13 @@ describe('Login page', () => { }) +describe('Forget password page', () => { + it('submit forget password form', () => { + cy.visit('/authentication/basic/forget-password') + cy.get('[data-testid=email]').type('peter@example.com') + cy.get('[data-testid=submit]').click() + cy.contains('An email is sent') + }) +}) + diff --git a/jam-ui/src/components/auth/ConfirmMailContent.js b/jam-ui/src/components/auth/ConfirmMailContent.js index d08db3d4c..a1a05a3ef 100644 --- a/jam-ui/src/components/auth/ConfirmMailContent.js +++ b/jam-ui/src/components/auth/ConfirmMailContent.js @@ -4,20 +4,30 @@ import { Button } from 'reactstrap'; import { Link } from 'react-router-dom'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import envelope from '../../assets/img/illustrations/envelope.png'; +import { useBrowserQuery } from '../../context/BrowserQuery'; +import { useTranslation } from 'react-i18next'; -const ConfirmMailContent = ({ layout, titleTag: TitleTag }) => ( - - sent - Please check your email! -

- An email has been sent to you. Please click on the included link to reset your password. -

- -
-); +const ConfirmMailContent = ({ layout, titleTag: TitleTag }) => { + const queryString = useBrowserQuery(); + const { t } = useTranslation('auth'); + + const toWording = queryString && queryString.get('email') ? queryString.get('email') : t('confirmEmailContent.toYou'); + + return ( + + sent + {t('confirmEmailContent.title')} +

{t('confirmEmailContent.description_1')} {toWording}. {t('confirmEmailContent.description_2')}

+

+ {t('confirmEmailContent.description_3')} +

+ +
+ ); +}; ConfirmMailContent.propTypes = { layout: PropTypes.string, diff --git a/jam-ui/src/components/auth/ForgetPasswordForm.js b/jam-ui/src/components/auth/ForgetPasswordForm.js index c1a08800b..97c82379e 100644 --- a/jam-ui/src/components/auth/ForgetPasswordForm.js +++ b/jam-ui/src/components/auth/ForgetPasswordForm.js @@ -5,12 +5,15 @@ import { toast } from 'react-toastify'; import { Button, Form, FormGroup, Input } from 'reactstrap'; import withRedirect from '../../hoc/withRedirect'; import { requstResetForgotPassword } from '../../helpers/rest'; +import { useTranslation } from 'react-i18next'; const ForgetPasswordForm = ({ setRedirect, setRedirectUrl, layout }) => { // State const [email, setEmail] = useState(''); const [submitting, setSubmitting] = useState(false); + const { t } = useTranslation('auth'); + // Handler const handleSubmit = e => { e.preventDefault(); @@ -29,35 +32,32 @@ const ForgetPasswordForm = ({ setRedirect, setRedirectUrl, layout }) => { }; useEffect(() => { - setRedirectUrl(`/authentication/${layout}/confirm-mail`); - }, [setRedirectUrl, layout]); + setRedirectUrl(`/authentication/${layout}/confirm-mail?email=${email}`); + }, [setRedirectUrl, layout, email]); return (
setEmail(target.value)} type="email" required disabled={submitting} + data-testid="email" /> - {/* I can't recover my account using this page */} - - I can't recover my account using this page - -
); }; diff --git a/jam-ui/src/components/auth/LoginForm.js b/jam-ui/src/components/auth/LoginForm.js index 12bd5bae5..6a45b16e9 100644 --- a/jam-ui/src/components/auth/LoginForm.js +++ b/jam-ui/src/components/auth/LoginForm.js @@ -8,6 +8,7 @@ import SocialAuthButtons from './SocialAuthButtons'; import withRedirect from '../../hoc/withRedirect'; import { useAuth } from '../../context/UserAuth'; //import { signin } from '../../services/authService'; +import { useTranslation } from 'react-i18next'; const LoginForm = ({ setRedirect, hasLabel, layout }) => { // State @@ -17,6 +18,8 @@ const LoginForm = ({ setRedirect, hasLabel, layout }) => { const [isDisabled, setIsDisabled] = useState(true); const history = useHistory(); + + const { t } = useTranslation('auth'); const location = useLocation(); let { from } = location.state || { from: { pathname: "/" } }; @@ -27,8 +30,8 @@ const LoginForm = ({ setRedirect, hasLabel, layout }) => { const handleSubmit = async e => { e.preventDefault(); const credentials = {email, password} + setIsDisabled(true); const user = await login(credentials) - console.log("handleSubmit", user); if(user){ setCurrentUser(user) //localStorage.setItem('user', user) @@ -40,7 +43,7 @@ const LoginForm = ({ setRedirect, hasLabel, layout }) => { }else{ toast.error("Incorrect email or password"); } - + setIsDisabled(false); }; useEffect(() => { @@ -50,20 +53,20 @@ const LoginForm = ({ setRedirect, hasLabel, layout }) => { return (
- {hasLabel && } + {hasLabel && } setEmail(target.value)} type="email" /> - {hasLabel && } + {hasLabel && } setPassword(target.value)} type="password" @@ -73,7 +76,7 @@ const LoginForm = ({ setRedirect, hasLabel, layout }) => { setRemember(target.checked)} type="checkbox" @@ -88,7 +91,7 @@ const LoginForm = ({ setRedirect, hasLabel, layout }) => { {/* or log in with diff --git a/jam-ui/src/components/auth/PasswordResetForm.js b/jam-ui/src/components/auth/PasswordResetForm.js index 833e186e9..f1b91d712 100644 --- a/jam-ui/src/components/auth/PasswordResetForm.js +++ b/jam-ui/src/components/auth/PasswordResetForm.js @@ -5,18 +5,42 @@ import { Button, Form, FormGroup, Input } from 'reactstrap'; import withRedirect from '../../hoc/withRedirect'; import Label from 'reactstrap/es/Label'; import classNames from 'classnames'; +import { useBrowserQuery } from '../../context/BrowserQuery'; +import { resetForgotPassword } from '../../helpers/rest'; +import { useTranslation } from 'react-i18next'; const PasswordResetForm = ({ setRedirect, setRedirectUrl, layout, hasLabel }) => { // State const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [isDisabled, setIsDisabled] = useState(true); + const queryString = useBrowserQuery(); + + const { t } = useTranslation('auth'); // Handler const handleSubmit = e => { e.preventDefault(); - toast.success('Login with your new password'); - setRedirect(true); + const token = queryString.get('token'); + const email = queryString.get('email'); + if (!token || !email) return; + + if (password !== confirmPassword) return; + const data = { email, token, password, password_confirmation: confirmPassword} + setIsDisabled(true); + resetForgotPassword(data) + .then(() => { + toast.success(t('resetForm.successMessage')); + setRedirect(true); + }) + .catch(error => { + error.json().then(data => { + toast.error(data.message); + }); + }) + .finally(() => { + setIsDisabled(false); + }); }; useEffect(() => { @@ -32,25 +56,25 @@ const PasswordResetForm = ({ setRedirect, setRedirectUrl, layout, hasLabel }) => return ( - {hasLabel && } + {hasLabel && } setPassword(target.value)} type="password" /> - {hasLabel && } + {hasLabel && } setConfirmPassword(target.value)} type="password" /> ); diff --git a/jam-ui/src/components/auth/basic/ForgetPassword.js b/jam-ui/src/components/auth/basic/ForgetPassword.js index 4ef4c8a05..cffa3007a 100644 --- a/jam-ui/src/components/auth/basic/ForgetPassword.js +++ b/jam-ui/src/components/auth/basic/ForgetPassword.js @@ -1,11 +1,12 @@ import React from 'react'; import ForgetPasswordForm from '../ForgetPasswordForm'; - +import { useTranslation } from 'react-i18next'; const ForgetPassword = () => { + const { t } = useTranslation('auth'); return (
-
Forgot your password?
- Enter your email and we'll send you a reset link. +
{ t('forgotForm.title')}
+ { t('forgotForm.description')}
); diff --git a/jam-ui/src/components/auth/basic/Login.js b/jam-ui/src/components/auth/basic/Login.js index 66a20a878..0ef134aa6 100644 --- a/jam-ui/src/components/auth/basic/Login.js +++ b/jam-ui/src/components/auth/basic/Login.js @@ -2,28 +2,27 @@ import React, { Fragment } from 'react'; import { Col, Row } from 'reactstrap'; import { Link } from 'react-router-dom'; import LoginForm from '../LoginForm'; -import { useTranslation } from "react-i18next"; +import { useTranslation } from 'react-i18next'; -//const {t} = useTranslation(); +const Login = () => { + const { t } = useTranslation('auth'); -const Login = () => ( - - - - - -
Sign in
- - -

- or {' '} - {/* create an account */} - Sign up -

- -
- -
-); + return ( + + + +
{t('signin')}
+ + +

+ or {/* create an account */} + {t('signup')} +

+ +
+ +
+ ); +}; export default Login; diff --git a/jam-ui/src/components/auth/basic/PasswordReset.js b/jam-ui/src/components/auth/basic/PasswordReset.js index 407512930..4c65faeb2 100644 --- a/jam-ui/src/components/auth/basic/PasswordReset.js +++ b/jam-ui/src/components/auth/basic/PasswordReset.js @@ -1,11 +1,16 @@ import React from 'react'; import PasswordResetForm from '../PasswordResetForm'; +import { useTranslation } from 'react-i18next'; -const PasswordReset = () => ( -
-
Reset new password
- -
-); +const PasswordReset = () => { + const { t } = useTranslation('auth'); + return ( +
+
{ t('resetForm.title')}
+ {t('resetForm.subTitle')} + +
+ ); +}; export default PasswordReset; diff --git a/jam-ui/src/i18n/locales/en/auth.json b/jam-ui/src/i18n/locales/en/auth.json index 75cee360e..10fd73ea4 100644 --- a/jam-ui/src/i18n/locales/en/auth.json +++ b/jam-ui/src/i18n/locales/en/auth.json @@ -1,5 +1,36 @@ { "signup": "Sign Up", "signin": "Sign In", - "signout": "Sign Out" + "signout": "Sign Out", + "signinForm": { + "email": "Email Address", + "password": "Password", + "remember": "Remember me", + "forgot": "Forgot password?", + "submit": "Sign In" + }, + "forgotForm": { + "title": "Forgot Your Password", + "description": "Enter your email address and we'll send you a link to reset your password.", + "email": "Email Address", + "submit": "Send Reset Link", + "submitting": "Sending Reset Link..." + }, + "confirmEmailContent": { + "title": "Please check your email!", + "description_1": "An email has been sent to ", + "toYou": "to you", + "description_2": "Please click on the included link to reset your password.", + "description_3": "If you don't see the email, check other places it might be, like your junk, spam, social, or other folders. If you still can't find it, you may not be entering the email address that you used when signing up with the service.", + "returnToLogin": "Return to Sign In" + }, + "resetForm": { + "title": "Reset Your Password", + "subTitle": "Enter your new password below.", + "password": "New Password", + "confirmPassword": "Confirm New Password", + "submit": "Reset Password", + "submitting": "Resetting Password...", + "successMessage": "Your password has been reset. Please sign in with your new password." + } } \ No newline at end of file diff --git a/jam-ui/src/layouts/JKAuthBasicLayout.js b/jam-ui/src/layouts/JKAuthBasicLayout.js index 69c331658..bcf764d26 100644 --- a/jam-ui/src/layouts/JKAuthBasicLayout.js +++ b/jam-ui/src/layouts/JKAuthBasicLayout.js @@ -4,16 +4,19 @@ import Logo from '../components/navbar/Logo'; import Section from '../components/common/Section'; import AuthBasicRoutes from '../components/auth/basic/JKAuthBasicRoutes'; import UserAuth from '../context/UserAuth'; +import { BrowserQueryProvider } from '../context/BrowserQuery'; -const AuthBasicLayout = ({location}) => ( +const AuthBasicLayout = ({ location }) => (
- - + + + + diff --git a/web/app/controllers/api_sessions_controller.rb b/web/app/controllers/api_sessions_controller.rb index d6c4067c5..f9c91af71 100644 --- a/web/app/controllers/api_sessions_controller.rb +++ b/web/app/controllers/api_sessions_controller.rb @@ -18,10 +18,11 @@ class ApiSessionsController < ApiController end end - #reset_password_token is updated. inteanded for the react app (spa) + #update password token. inteanded for the react app (spa) def request_reset_password begin - User.reset_password(params[:email], APP_CONFIG.spa_origin) + url = APP_CONFIG.spa_origin + '/authentication/basic' + User.reset_password(params[:email], url) render :json => {}, :status => 204 rescue JamRuby::JamArgumentError render :json => {:message => ValidationMessages::EMAIL_NOT_FOUND}, :status => 403 @@ -29,4 +30,13 @@ class ApiSessionsController < ApiController end + def reset_forgot_password + begin + User.set_password_from_token(params[:email], params[:token], params[:password], params[:password_confirmation]) + render :json => {}, :status => 204 + rescue JamRuby::JamArgumentError => e + render :json => {:message => e.field_message}, :status => 403 + end + end + end diff --git a/web/config/routes.rb b/web/config/routes.rb index 6e0ccd6df..2e2738135 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -411,7 +411,7 @@ Rails.application.routes.draw do #reset forgot password (not logged in) post '/request_reset_forgot_password', to: 'api_sessions#request_reset_password' - post '/reset_forgot_password', to: 'users#reset_forgot_password' + post '/reset_forgot_password', to: 'api_sessions#reset_forgot_password' match '/reviews' => 'api_reviews#index', :via => :get match '/reviews' => 'api_reviews#create', :via => :post