forgot password feature
This commit is contained in:
parent
df9cbf3ba7
commit
ec7a79c8bb
|
|
@ -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.')
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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">→</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">→</span>
|
||||
</a>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -88,7 +91,7 @@ const LoginForm = ({ setRedirect, hasLabel, layout }) => {
|
|||
</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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue