Skip to content

Commit 4eeec58

Browse files
committed
Add support for password reset links
1 parent 54cfe7e commit 4eeec58

File tree

12 files changed

+266
-43
lines changed

12 files changed

+266
-43
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"feather-icons": "^4.10.0",
1111
"jquery": "^3.3.1",
1212
"lodash": "^4.17.11",
13+
"query-string": "^6.7.0",
1314
"react": "^16.8.6",
1415
"react-dom": "^16.8.6",
1516
"react-hot-loader": "^4.9.0",
@@ -30,6 +31,7 @@
3031
"@types/classnames": "^2.2.8",
3132
"@types/feather-icons": "^4.7.0",
3233
"@types/lodash": "^4.14.119",
34+
"@types/query-string": "^6.3.0",
3335
"@types/react": "^16.8.19",
3436
"@types/react-dom": "^16.8.4",
3537
"@types/react-router-dom": "^4.3.3",

resources/assets/styles/components/authentication.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.login-box {
2-
@apply .bg-white .shadow-lg .rounded-lg .pt-10 .px-8 .pb-6 .mb-4;
2+
@apply .bg-white .shadow-lg .rounded-lg .p-6;
33

44
@screen xsx {
55
@apply .rounded-none;

resources/assets/styles/components/forms.css

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ input[type=number] {
1717
* is input and then sinks back down into the field if left empty.
1818
*/
1919
.input-open {
20-
@apply .w-full .px-3 .relative;
20+
@apply .w-full .relative;
2121
}
2222

23-
.input-open > .input {
24-
@apply .appearance-none .block .w-full .text-neutral-800 .border-b-2 .border-neutral-200 .py-3 .mb-3;
23+
.input-open > .input, .input-open > .input:disabled {
24+
@apply .appearance-none .block .w-full .text-neutral-800 .border-b-2 .border-neutral-200 .py-3 .px-2 .bg-white;
2525

2626
&:focus {
2727
@apply .border-primary-400;
@@ -40,9 +40,9 @@ input[type=number] {
4040
}
4141

4242
.input-open > label {
43-
@apply .block .uppercase .tracking-wide .text-neutral-500 .text-xs .mb-2 .absolute;
43+
@apply .block .uppercase .tracking-wide .text-neutral-500 .text-xs .mb-2 .px-2 .absolute;
4444
top: 14px;
45-
transition: transform 200ms ease-out;
45+
transition: padding 200ms linear, transform 200ms ease-out;
4646
}
4747

4848
/**
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import http from '@/api/http';
2+
3+
interface Data {
4+
token: string;
5+
password: string;
6+
passwordConfirmation: string;
7+
}
8+
9+
interface PasswordResetResponse {
10+
redirectTo?: string | null;
11+
sendToLogin: boolean;
12+
}
13+
14+
export default (email: string, data: Data): Promise<PasswordResetResponse> => {
15+
return new Promise((resolve, reject) => {
16+
http.post('/auth/password/reset', {
17+
email,
18+
token: data.token,
19+
password: data.password,
20+
// eslint-disable-next-line @typescript-eslint/camelcase
21+
password_confirmation: data.passwordConfirmation,
22+
})
23+
.then(response => resolve({
24+
redirectTo: response.data.redirect_to,
25+
sendToLogin: response.data.send_to_login,
26+
}))
27+
.catch(reject);
28+
});
29+
};

resources/scripts/components/FlashMessageRender.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
44
import MessageBox from '@/components/MessageBox';
55

66
type Props = Readonly<{
7+
spacerClass?: string;
78
flashes: FlashMessage[];
89
}>;
910

@@ -16,18 +17,17 @@ class FlashMessageRender extends React.PureComponent<Props> {
1617
return (
1718
<React.Fragment>
1819
{
19-
this.props.flashes.map(flash => (
20-
<MessageBox
21-
key={flash.id || flash.type + flash.message}
22-
type={flash.type}
23-
title={flash.title}
24-
>
25-
{flash.message}
26-
</MessageBox>
20+
this.props.flashes.map((flash, index) => (
21+
<React.Fragment key={flash.id || flash.type + flash.message}>
22+
{index > 0 && <div className={this.props.spacerClass || 'mt-2'}></div>}
23+
<MessageBox type={flash.type} title={flash.title}>
24+
{flash.message}
25+
</MessageBox>
26+
</React.Fragment>
2727
))
2828
}
2929
</React.Fragment>
30-
)
30+
);
3131
}
3232
}
3333

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as React from 'react';
2+
import MessageBox from '@/components/MessageBox';
3+
4+
export default ({ message }: { message: string | undefined | null }) => (
5+
!message ?
6+
null
7+
:
8+
<div className={'mb-4'}>
9+
<MessageBox type={'error'} title={'Error'}>
10+
{message}
11+
</MessageBox>
12+
</div>
13+
);

resources/scripts/components/auth/ForgotPasswordContainer.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,12 @@ class ForgotPasswordContainer extends React.PureComponent<Props, State> {
5858

5959
render () {
6060
return (
61-
<React.Fragment>
61+
<div>
62+
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
63+
Request Password Reset
64+
</h2>
6265
<form className={'login-box'} onSubmit={this.handleSubmission}>
63-
<div className={'-mx-3'}>
66+
<div className={'mt-3'}>
6467
<OpenInputField
6568
ref={this.emailField}
6669
id={'email'}
@@ -93,18 +96,14 @@ class ForgotPasswordContainer extends React.PureComponent<Props, State> {
9396
</Link>
9497
</div>
9598
</form>
96-
</React.Fragment>
99+
</div>
97100
);
98101
}
99102
}
100103

101-
const mapStateToProps = (state: ReduxState) => ({
102-
flashes: state.flashes,
103-
});
104-
105104
const mapDispatchToProps = {
106105
pushFlashMessage,
107106
clearAllFlashMessages,
108107
};
109108

110-
export default connect(mapStateToProps, mapDispatchToProps)(ForgotPasswordContainer);
109+
export default connect(null, mapDispatchToProps)(ForgotPasswordContainer);

resources/scripts/components/auth/LoginContainer.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import OpenInputField from '@/components/forms/OpenInputField';
33
import { Link } from 'react-router-dom';
44
import login from '@/api/auth/login';
55
import { httpErrorToHuman } from '@/api/http';
6-
import MessageBox from '@/components/MessageBox';
6+
import NetworkErrorMessage from '@/components/NetworkErrorMessage';
77

88
type State = Readonly<{
99
errorMessage?: string;
@@ -52,15 +52,12 @@ export default class LoginContainer extends React.PureComponent<{}, State> {
5252
render () {
5353
return (
5454
<React.Fragment>
55-
{this.state.errorMessage &&
56-
<div className={'mb-4'}>
57-
<MessageBox type={'error'} title={'Error'}>
58-
{this.state.errorMessage}
59-
</MessageBox>
60-
</div>
61-
}
55+
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
56+
Login to Continue
57+
</h2>
58+
<NetworkErrorMessage message={this.state.errorMessage}/>
6259
<form className={'login-box'} onSubmit={this.submit}>
63-
<div className={'-mx-3'}>
60+
<div className={'mt-3'}>
6461
<OpenInputField
6562
autoFocus={true}
6663
label={'Username or Email'}
@@ -71,7 +68,7 @@ export default class LoginContainer extends React.PureComponent<{}, State> {
7168
disabled={this.state.isLoading}
7269
/>
7370
</div>
74-
<div className={'-mx-3 mt-6'}>
71+
<div className={'mt-6'}>
7572
<OpenInputField
7673
label={'Password'}
7774
type={'password'}
@@ -96,7 +93,7 @@ export default class LoginContainer extends React.PureComponent<{}, State> {
9693
</div>
9794
<div className={'mt-6 text-center'}>
9895
<Link
99-
to={'/forgot-password'}
96+
to={'/password'}
10097
className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'}
10198
>
10299
Forgot password?
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import * as React from 'react';
2+
import OpenInputField from '@/components/forms/OpenInputField';
3+
import { RouteComponentProps } from 'react-router';
4+
import { parse } from 'query-string';
5+
import { Link } from 'react-router-dom';
6+
import NetworkErrorMessage from '@/components/NetworkErrorMessage';
7+
import performPasswordReset from '@/api/auth/performPasswordReset';
8+
import { httpErrorToHuman } from '@/api/http';
9+
import { connect } from 'react-redux';
10+
import { pushFlashMessage, clearAllFlashMessages } from '@/redux/actions/flash';
11+
12+
type State = Readonly<{
13+
email?: string;
14+
password?: string;
15+
passwordConfirm?: string;
16+
isLoading: boolean;
17+
errorMessage?: string;
18+
}>;
19+
20+
type Props = Readonly<RouteComponentProps<{ token: string }> & {
21+
pushFlashMessage: typeof pushFlashMessage;
22+
clearAllFlashMessages: typeof clearAllFlashMessages;
23+
}>;
24+
25+
class ResetPasswordContainer extends React.PureComponent<Props, State> {
26+
state: State = {
27+
isLoading: false,
28+
};
29+
30+
componentDidMount () {
31+
const parsed = parse(this.props.location.search);
32+
33+
this.setState({ email: parsed.email as string || undefined });
34+
}
35+
36+
canSubmit () {
37+
if (!this.state.password || !this.state.email) {
38+
return false;
39+
}
40+
41+
return this.state.password.length >= 8 && this.state.password === this.state.passwordConfirm;
42+
}
43+
44+
onPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => this.setState({
45+
password: e.target.value,
46+
});
47+
48+
onPasswordConfirmChange = (e: React.ChangeEvent<HTMLInputElement>) => this.setState({
49+
passwordConfirm: e.target.value,
50+
});
51+
52+
onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
53+
e.preventDefault();
54+
55+
const { password, passwordConfirm, email } = this.state;
56+
if (!password || !email || !passwordConfirm) {
57+
return;
58+
}
59+
60+
this.props.clearAllFlashMessages();
61+
this.setState({ isLoading: true }, () => {
62+
performPasswordReset(email, {
63+
token: this.props.match.params.token,
64+
password: password,
65+
passwordConfirmation: passwordConfirm,
66+
})
67+
.then(response => {
68+
if (response.redirectTo) {
69+
// @ts-ignore
70+
window.location = response.redirectTo;
71+
return;
72+
}
73+
74+
this.props.pushFlashMessage({
75+
type: 'success',
76+
message: 'Your password has been reset, please login to continue.',
77+
});
78+
this.props.history.push('/login');
79+
})
80+
.catch(error => {
81+
console.error(error);
82+
this.setState({ errorMessage: httpErrorToHuman(error) });
83+
})
84+
.then(() => this.setState({ isLoading: false }));
85+
});
86+
};
87+
88+
render () {
89+
return (
90+
<div>
91+
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
92+
Reset Password
93+
</h2>
94+
<NetworkErrorMessage message={this.state.errorMessage}/>
95+
<form className={'login-box'} onSubmit={this.onSubmit}>
96+
<div className={'mt-3'}>
97+
<OpenInputField
98+
label={'Email'}
99+
value={this.state.email || ''}
100+
disabled
101+
/>
102+
</div>
103+
<div className={'mt-6'}>
104+
<OpenInputField
105+
autoFocus={true}
106+
label={'New Password'}
107+
description={'Passwords must be at least 8 characters in length.'}
108+
type={'password'}
109+
required={true}
110+
id={'password'}
111+
onChange={this.onPasswordChange}
112+
/>
113+
</div>
114+
<div className={'mt-6'}>
115+
<OpenInputField
116+
label={'Confirm New Password'}
117+
type={'password'}
118+
required={true}
119+
id={'password-confirm'}
120+
onChange={this.onPasswordConfirmChange}
121+
/>
122+
</div>
123+
<div className={'mt-6'}>
124+
<button
125+
type={'submit'}
126+
className={'btn btn-primary btn-jumbo'}
127+
disabled={this.state.isLoading || !this.canSubmit()}
128+
>
129+
{this.state.isLoading ?
130+
<span className={'spinner white'}>&nbsp;</span>
131+
:
132+
'Reset Password'
133+
}
134+
</button>
135+
</div>
136+
<div className={'mt-6 text-center'}>
137+
<Link
138+
to={'/login'}
139+
className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'}
140+
>
141+
Return to Login
142+
</Link>
143+
</div>
144+
</form>
145+
</div>
146+
);
147+
}
148+
}
149+
150+
const mapDispatchToProps = {
151+
pushFlashMessage,
152+
clearAllFlashMessages,
153+
};
154+
155+
export default connect(null, mapDispatchToProps)(ResetPasswordContainer);

0 commit comments

Comments
 (0)