forgot password feature

This commit is contained in:
Nuwan 2024-09-28 22:11:48 +05:30
parent 60d64f5fa0
commit 6f3154a3d3
13 changed files with 226 additions and 77 deletions

View File

@ -0,0 +1,54 @@
///<reference types="cypress" />
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.')
});
});
});

View File

@ -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')
})
})

View File

@ -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 }) => (
<Fragment>
<img className="d-block mx-auto mb-4" src={envelope} alt="sent" width={70} />
<TitleTag>Please check your email!</TitleTag>
<p>
An email has been sent to you. Please click on the included link to reset your password.
</p>
<Button tag={Link} color="primary" size="sm" className="mt-3" to={`/authentication/${layout}/login`}>
<FontAwesomeIcon icon="chevron-left" transform="shrink-4 down-1" className="mr-1" />
Return to login
</Button>
</Fragment>
);
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 (
<Fragment>
<img className="d-block mx-auto mb-4" src={envelope} alt="sent" width={70} />
<TitleTag>{t('confirmEmailContent.title')}</TitleTag>
<p>{t('confirmEmailContent.description_1')} {toWording}. {t('confirmEmailContent.description_2')}</p>
<p>
{t('confirmEmailContent.description_3')}
</p>
<Button tag={Link} color="primary" size="sm" className="mt-3" to={`/authentication/${layout}/login`}>
<FontAwesomeIcon icon="chevron-left" transform="shrink-4 down-1" className="mr-1" />
{t('confirmEmailContent.returnToLogin')}
</Button>
</Fragment>
);
};
ConfirmMailContent.propTypes = {
layout: PropTypes.string,

View File

@ -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 (
<Form className="mt-4" onSubmit={handleSubmit}>
<FormGroup>
<Input
className="form-control"
placeholder="Email address"
placeholder={t('forgotForm.email')}
value={email}
onChange={({ target }) => setEmail(target.value)}
type="email"
required
disabled={submitting}
data-testid="email"
/>
</FormGroup>
<FormGroup>
<Button color="primary" block disabled={!email || submitting}>
Send reset link
<Button color="primary" block disabled={!email || submitting} data-testid="submit">
{submitting ? t('forgotForm.submitting') : t('forgotForm.submit')}
</Button>
</FormGroup>
{/* <Link className="fs--1 text-600" to="#!">
I can't recover my account using this page
<span className="d-inline-block ml-1">&rarr;</span>
</Link> */}
<a href="https://www.jamkazam.com/help_desk" target="_blank" rel="noopener noreferrer" className="fs--1 text-600">
I can't recover my account using this page
<span className="d-inline-block ml-1">&rarr;</span>
</a>
</Form>
);
};

View File

@ -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 (
<Form onSubmit={handleSubmit}>
<FormGroup>
{hasLabel && <Label>Email address</Label>}
{hasLabel && <Label>{ t('signinForm.email')}</Label>}
<Input
data-testid="email"
placeholder={!hasLabel ? 'Email address' : ''}
placeholder={!hasLabel ? t('signinForm.email') : ''}
value={email}
onChange={({ target }) => setEmail(target.value)}
type="email"
/>
</FormGroup>
<FormGroup>
{hasLabel && <Label>Password</Label>}
{hasLabel && <Label>{t('signinForm.password')}</Label>}
<Input
data-testid="password"
placeholder={!hasLabel ? 'Password' : ''}
placeholder={!hasLabel ? t('signinForm.password') : ''}
value={password}
onChange={({ target }) => setPassword(target.value)}
type="password"
@ -73,7 +76,7 @@ const LoginForm = ({ setRedirect, hasLabel, layout }) => {
<Col xs="auto">
<CustomInput
id="customCheckRemember"
label="Remember me"
label={ t('signinForm.remember') }
checked={remember}
onChange={({ target }) => setRemember(target.checked)}
type="checkbox"
@ -81,13 +84,13 @@ const LoginForm = ({ setRedirect, hasLabel, layout }) => {
</Col>
<Col xs="auto">
<Link className="fs--1" to={`/authentication/${layout}/forget-password`}>
Forget Password?
{ t('signinForm.forgot') }
</Link>
</Col>
</Row>
<FormGroup>
<Button color="primary" block className="mt-3" data-testid="submit" disabled={isDisabled}>
Sign in
{ t('signinForm.submit') }
</Button>
</FormGroup>
{/* <Divider className="mt-4">or log in with</Divider>

View File

@ -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 (
<Form className={classNames('mt-3', { 'text-left': hasLabel })} onSubmit={handleSubmit}>
<FormGroup>
{hasLabel && <Label>New Password</Label>}
{hasLabel && <Label>{t('resetForm.password')}</Label>}
<Input
placeholder={!hasLabel ? 'New Password' : ''}
placeholder={!hasLabel ? t('resetForm.password') : ''}
value={password}
onChange={({ target }) => setPassword(target.value)}
type="password"
/>
</FormGroup>
<FormGroup>
{hasLabel && <Label>Confirm Password</Label>}
{hasLabel && <Label>{t('resetForm.confirmPassword')}</Label>}
<Input
placeholder={!hasLabel ? 'Confirm Password' : ''}
placeholder={!hasLabel ? t('resetForm.confirmPassword') : ''}
value={confirmPassword}
onChange={({ target }) => setConfirmPassword(target.value)}
type="password"
/>
</FormGroup>
<Button color="primary" block className="mt-3" disabled={isDisabled}>
Set password
{t('resetForm.submit')}
</Button>
</Form>
);

View File

@ -1,11 +1,12 @@
import React from 'react';
import ForgetPasswordForm from '../ForgetPasswordForm';
import { useTranslation } from 'react-i18next';
const ForgetPassword = () => {
const { t } = useTranslation('auth');
return (
<div className="text-center">
<h5 className="mb-0"> Forgot your password?</h5>
<small>Enter your email and we'll send you a reset link.</small>
<h5 className="mb-0">{ t('forgotForm.title')}</h5>
<small>{ t('forgotForm.description')}</small>
<ForgetPasswordForm />
</div>
);

View File

@ -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 = () => (
<Fragment>
<Row className="text-left justify-content-between">
<Col xs="auto">
<h5>Sign in</h5>
</Col>
<Col xs="auto">
<p className="fs--1 text-600">
or {' '}
{/* <Link to="/authentication/basic/register">create an account</Link> */}
<a href={`${process.env.REACT_APP_CLIENT_BASE_URL}/signup`}>Sign up</a>
</p>
</Col>
</Row>
<LoginForm />
</Fragment>
);
return (
<Fragment>
<Row className="text-left justify-content-between">
<Col xs="auto">
<h5>{t('signin')}</h5>
</Col>
<Col xs="auto">
<p className="fs--1 text-600">
or {/* <Link to="/authentication/basic/register">create an account</Link> */}
<a href={`${process.env.REACT_APP_CLIENT_BASE_URL}/signup`}>{t('signup')}</a>
</p>
</Col>
</Row>
<LoginForm />
</Fragment>
);
};
export default Login;

View File

@ -1,11 +1,16 @@
import React from 'react';
import PasswordResetForm from '../PasswordResetForm';
import { useTranslation } from 'react-i18next';
const PasswordReset = () => (
<div className="text-center">
<h5>Reset new password</h5>
<PasswordResetForm />
</div>
);
const PasswordReset = () => {
const { t } = useTranslation('auth');
return (
<div className="text-center">
<h5>{ t('resetForm.title')}</h5>
<small>{t('resetForm.subTitle')}</small>
<PasswordResetForm />
</div>
);
};
export default PasswordReset;

View File

@ -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."
}
}

View File

@ -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 }) => (
<Section className="py-0">
<Row className="flex-center min-vh-100 py-6">
<Col sm={10} md={8} lg={6} xl={5} className="col-xxl-4">
<Logo width={200} />
<Card>
<CardBody className="fs--1 font-weight-normal p-5">
<UserAuth path={location.pathname}>
<AuthBasicRoutes />
<UserAuth path={location.pathname}>
<BrowserQueryProvider>
<AuthBasicRoutes />
</BrowserQueryProvider>
</UserAuth>
</CardBody>
</Card>

View File

@ -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

View File

@ -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