Skip to content

Commit 9b02283

Browse files
author
Jovert Lota Palonpon
committed
Completed Password Reset, resolve #39
1 parent be2f079 commit 9b02283

File tree

13 files changed

+260
-54
lines changed

13 files changed

+260
-54
lines changed

app/Http/Controllers/Api/V1/Auth/ResetPasswordController.php

+39-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
namespace App\Http\Controllers\Api\V1\Auth;
44

5+
use App\User;
56
use Illuminate\Http\Request;
67
use Illuminate\Http\JsonResponse;
78
use Illuminate\Support\Facades\DB;
9+
use Illuminate\Contracts\Auth\Guard;
10+
use Illuminate\Support\Facades\Auth;
811
use App\Http\Controllers\Controller;
912

1013
class ResetPasswordController extends Controller
@@ -16,8 +19,42 @@ class ResetPasswordController extends Controller
1619
*
1720
* @return \Illuminate\Http\JsonResponse
1821
*/
19-
public function reset(Request $request) : JsonResponse
22+
public function reset(Request $request, string $token) : JsonResponse
2023
{
21-
return response()->json('Resetting...');
24+
$request->validate([
25+
'password' => 'required|min:8|confirmed|pwned:100'
26+
]);
27+
28+
$password_reset = DB::table('password_resets')
29+
->where('token', $token)
30+
->latest()
31+
->first();
32+
33+
if (! $password_reset) {
34+
return response()->json('Reset link invalid!', 422);
35+
}
36+
37+
$user = User::where('email', $password_reset->email)->first();
38+
39+
if (! $password_reset) {
40+
return response()->json('User does not exist!', 422);
41+
}
42+
43+
$user->password = bcrypt($request->input('password'));
44+
$user->update();
45+
46+
return response()->json(
47+
_token_payload($this->guard()->login($user))
48+
);
49+
}
50+
51+
/**
52+
* Get the guard to be used during authentication.
53+
*
54+
* @return \Illuminate\Contracts\Auth\Guard
55+
*/
56+
protected function guard() : Guard
57+
{
58+
return Auth::guard('api');
2259
}
2360
}

app/Http/Controllers/Api/V1/Auth/SessionsController.php

+1-5
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,7 @@ protected function respondWithToken($authToken) : JsonResponse
103103

104104
$this->saveAuthToken($authToken, $user);
105105

106-
return response()->json([
107-
'auth_token' => $authToken,
108-
'token_type' => 'bearer',
109-
'expires_in' => $this->guard()->factory()->getTTL() * 60
110-
]);
106+
return response()->json(_token_payload($authToken));
111107
}
112108

113109
/**

app/Utils/Helper.php

+19-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,24 @@ function _asset(string $subPath) : string
1818
}
1919
}
2020

21+
if (! function_exists('_token_payload')) {
22+
/**
23+
* Get the token bearer payload.
24+
*
25+
* @param string $authToken
26+
*
27+
* @return array
28+
*/
29+
function _token_payload(string $authToken) : array
30+
{
31+
return [
32+
'auth_token' => $authToken,
33+
'token_type' => 'bearer',
34+
'expires_in' => auth()->guard('api')->factory()->getTTL() * 60
35+
];
36+
}
37+
}
38+
2139
if (! function_exists('_test_user')) {
2240
/**
2341
* Login and get the then authenticated user.
@@ -40,7 +58,7 @@ function _test_user()
4058
*
4159
* @return string $operator
4260
*/
43-
function _to_sql_operator($keyword)
61+
function _to_sql_operator($keyword) : string
4462
{
4563
switch ($keyword) {
4664
case 'eqs':

composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"laravel/framework": "5.8.*",
2121
"laravel/telescope": "^2.0",
2222
"laravel/tinker": "^1.0",
23-
"tymon/jwt-auth": "dev-develop#34d8e48 as 1.0.0-rc.3.2"
23+
"tymon/jwt-auth": "dev-develop#34d8e48 as 1.0.0-rc.3.2",
24+
"valorin/pwned-validator": "^1.2"
2425
},
2526
"require-dev": {
2627
"beyondcode/laravel-dump-server": "^1.0",

composer.lock

+46-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

resources/js/views/auth/passwords/Request.js

+12-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@ import React, { Component } from 'react';
22
import { Link as RouterLink } from 'react-router-dom';
33
import { Formik, Form, withFormik } from 'formik';
44
import * as Yup from 'yup';
5-
import { Grid, TextField, Button, Link, withStyles } from '@material-ui/core';
5+
import {
6+
Button,
7+
Grid,
8+
Link,
9+
TextField,
10+
Typography,
11+
withStyles,
12+
} from '@material-ui/core';
613

714
import * as NavigationUtils from '../../../utils/Navigation';
815
import * as UrlUtils from '../../../utils/URL';
@@ -43,10 +50,10 @@ class PasswordRequest extends Component {
4350
type: 'success',
4451
title: 'Link Sent',
4552
body: (
46-
<h4>
53+
<Typography>
4754
Check your email to reset your account.
4855
<br /> Thank you.
49-
</h4>
56+
</Typography>
5057
),
5158
action: () => history.push(`/signin?username=${email}`),
5259
},
@@ -59,10 +66,10 @@ class PasswordRequest extends Component {
5966
type: 'error',
6067
title: 'Something went wrong',
6168
body: (
62-
<h4>
69+
<Typography>
6370
Oops? Something went wrong here.
6471
<br /> Please try again.
65-
</h4>
72+
</Typography>
6673
),
6774
action: () => window.location.reload(),
6875
},

resources/js/views/auth/passwords/Reset.js

+48-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { Component } from 'react';
22
import { Link as RouterLink } from 'react-router-dom';
3-
import { Formik, Form, withFormik } from 'formik';
3+
import { Formik, Form } from 'formik';
44
import * as Yup from 'yup';
55

66
import {
@@ -26,6 +26,7 @@ class PasswordReset extends Component {
2626
state = {
2727
loading: false,
2828
message: {},
29+
email: '',
2930
showPassword: false,
3031
showPasswordConfirmation: false,
3132
};
@@ -52,21 +53,59 @@ class PasswordReset extends Component {
5253
*
5354
* @return {undefined}
5455
*/
55-
handleSubmit = async (values, { setSubmitting }) => {
56+
handleSubmit = async (values, { setSubmitting, setErrors }) => {
5657
setSubmitting(false);
58+
59+
this.setState({ loading: true });
60+
61+
try {
62+
const { match, pageProps } = this.props;
63+
const { token } = match.params;
64+
65+
const response = await axios.patch(
66+
`api/v1/auth/password/reset/${token}`,
67+
values,
68+
);
69+
70+
await pageProps.authenticate(JSON.stringify(response.data));
71+
72+
this.setState({ loading: false });
73+
} catch (error) {
74+
if (!error.response) {
75+
throw new Error('Unknown error');
76+
}
77+
78+
const { errors } = error.response.data;
79+
80+
if (errors) {
81+
setErrors(errors);
82+
}
83+
84+
this.setState({ loading: false });
85+
}
5786
};
5887

88+
componentDidMount() {
89+
const { location } = this.props;
90+
91+
const queryParams = UrlUtils._queryParams(location.search);
92+
93+
if (!queryParams.hasOwnProperty('email')) {
94+
return;
95+
}
96+
97+
this.setState({
98+
email: queryParams.email,
99+
});
100+
}
101+
59102
render() {
60-
const { classes, location } = this.props;
61-
const email = UrlUtils._queryParams(location.search).hasOwnProperty(
62-
'email',
63-
)
64-
? UrlUtils._queryParams(location.search).email
65-
: '';
103+
const { classes } = this.props;
66104

67105
const {
68106
loading,
69107
message,
108+
email,
70109
showPassword,
71110
showPasswordConfirmation,
72111
} = this.state;
@@ -265,4 +304,4 @@ const styles = theme => ({
265304
},
266305
});
267306

268-
export default withStyles(styles)(withFormik({})(PasswordReset));
307+
export default withStyles(styles)(PasswordReset);

resources/lang/en/validation.php

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
'not_regex' => 'The :attribute format is invalid.',
9393
'numeric' => 'The :attribute must be a number.',
9494
'present' => 'The :attribute field must be present.',
95+
'pwned' => 'The :attribute is weak, please enter a strong password.',
9596
'regex' => 'The :attribute format is invalid.',
9697
'required' => 'The :attribute field is required.',
9798
'required_if' => 'The :attribute field is required when :other is :value.',

resources/lang/fil/validation.php

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
'not_regex' => 'The :attribute format is invalid.',
9393
'numeric' => 'The :attribute must be a number.',
9494
'present' => 'The :attribute field must be present.',
95+
'pwned' => 'The :attribute is weak, please enter a strong password.',
9596
'regex' => 'The :attribute format is invalid.',
9697
'required' => 'The :attribute field is required.',
9798
'required_if' => 'The :attribute field is required when :other is :value.',

routes/api.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
Route::name('password.')->prefix('password')->group(function () {
2727
Route::post('request', 'ForgotPasswordController@sendResetLinkEmail')->name('request');
28-
Route::post('reset/{token}', 'ResetPasswordController@reset')->name('reset');
28+
Route::patch('reset/{token}', 'ResetPasswordController@reset')->name('reset');
2929
});
3030
});
3131

tests/Feature/Api/V1/Auth/ForgotPasswordTest.php

+22-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,30 @@
33
namespace Tests\Feature\Api\V1\Auth;
44

55
use App\User;
6+
use App\Jobs\ProcessPasswordResetRequest;
7+
use Illuminate\Support\Facades\Bus;
68
use Tests\Feature\Api\V1\BaseTest;
79

810
class ForgotPasswordTest extends BaseTest
911
{
12+
/** @test */
13+
public function a_user_can_request_for_password_reset_link()
14+
{
15+
// The user that is requesting for the Password Reset Link.
16+
$user = User::first();
1017

11-
}
18+
// The response body that should be sent alongside the request.
19+
$body = [
20+
'email' => $user->email
21+
];
22+
23+
// Assuming that the Password Reset Request is processed,
24+
// It must return a 200 response status and then,
25+
// It must return a response body containing: `Sending...`.
26+
$this->post(route('api.v1.auth.password.request'), $body)
27+
->assertStatus(200)
28+
->assertSee('Sending...');
29+
30+
// TODO: Assert if Jobs are dispatched & Notifications are sent.
31+
}
32+
}

0 commit comments

Comments
 (0)