Skip to content

Commit

Permalink
Completed Password Reset, resolve #39
Browse files Browse the repository at this point in the history
  • Loading branch information
Jovert Lota Palonpon committed Apr 7, 2019
1 parent be2f079 commit 9b02283
Show file tree
Hide file tree
Showing 13 changed files with 260 additions and 54 deletions.
41 changes: 39 additions & 2 deletions app/Http/Controllers/Api/V1/Auth/ResetPasswordController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

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

use App\User;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;

class ResetPasswordController extends Controller
Expand All @@ -16,8 +19,42 @@ class ResetPasswordController extends Controller
*
* @return \Illuminate\Http\JsonResponse
*/
public function reset(Request $request) : JsonResponse
public function reset(Request $request, string $token) : JsonResponse
{
return response()->json('Resetting...');
$request->validate([
'password' => 'required|min:8|confirmed|pwned:100'
]);

$password_reset = DB::table('password_resets')
->where('token', $token)
->latest()
->first();

if (! $password_reset) {
return response()->json('Reset link invalid!', 422);
}

$user = User::where('email', $password_reset->email)->first();

if (! $password_reset) {
return response()->json('User does not exist!', 422);
}

$user->password = bcrypt($request->input('password'));
$user->update();

return response()->json(
_token_payload($this->guard()->login($user))
);
}

/**
* Get the guard to be used during authentication.
*
* @return \Illuminate\Contracts\Auth\Guard
*/
protected function guard() : Guard
{
return Auth::guard('api');
}
}
6 changes: 1 addition & 5 deletions app/Http/Controllers/Api/V1/Auth/SessionsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,7 @@ protected function respondWithToken($authToken) : JsonResponse

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

return response()->json([
'auth_token' => $authToken,
'token_type' => 'bearer',
'expires_in' => $this->guard()->factory()->getTTL() * 60
]);
return response()->json(_token_payload($authToken));
}

/**
Expand Down
20 changes: 19 additions & 1 deletion app/Utils/Helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,24 @@ function _asset(string $subPath) : string
}
}

if (! function_exists('_token_payload')) {
/**
* Get the token bearer payload.
*
* @param string $authToken
*
* @return array
*/
function _token_payload(string $authToken) : array
{
return [
'auth_token' => $authToken,
'token_type' => 'bearer',
'expires_in' => auth()->guard('api')->factory()->getTTL() * 60
];
}
}

if (! function_exists('_test_user')) {
/**
* Login and get the then authenticated user.
Expand All @@ -40,7 +58,7 @@ function _test_user()
*
* @return string $operator
*/
function _to_sql_operator($keyword)
function _to_sql_operator($keyword) : string
{
switch ($keyword) {
case 'eqs':
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"laravel/framework": "5.8.*",
"laravel/telescope": "^2.0",
"laravel/tinker": "^1.0",
"tymon/jwt-auth": "dev-develop#34d8e48 as 1.0.0-rc.3.2"
"tymon/jwt-auth": "dev-develop#34d8e48 as 1.0.0-rc.3.2",
"valorin/pwned-validator": "^1.2"
},
"require-dev": {
"beyondcode/laravel-dump-server": "^1.0",
Expand Down
47 changes: 46 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 12 additions & 5 deletions resources/js/views/auth/passwords/Request.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import React, { Component } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { Formik, Form, withFormik } from 'formik';
import * as Yup from 'yup';
import { Grid, TextField, Button, Link, withStyles } from '@material-ui/core';
import {
Button,
Grid,
Link,
TextField,
Typography,
withStyles,
} from '@material-ui/core';

import * as NavigationUtils from '../../../utils/Navigation';
import * as UrlUtils from '../../../utils/URL';
Expand Down Expand Up @@ -43,10 +50,10 @@ class PasswordRequest extends Component {
type: 'success',
title: 'Link Sent',
body: (
<h4>
<Typography>
Check your email to reset your account.
<br /> Thank you.
</h4>
</Typography>
),
action: () => history.push(`/signin?username=${email}`),
},
Expand All @@ -59,10 +66,10 @@ class PasswordRequest extends Component {
type: 'error',
title: 'Something went wrong',
body: (
<h4>
<Typography>
Oops? Something went wrong here.
<br /> Please try again.
</h4>
</Typography>
),
action: () => window.location.reload(),
},
Expand Down
57 changes: 48 additions & 9 deletions resources/js/views/auth/passwords/Reset.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { Formik, Form, withFormik } from 'formik';
import { Formik, Form } from 'formik';
import * as Yup from 'yup';

import {
Expand All @@ -26,6 +26,7 @@ class PasswordReset extends Component {
state = {
loading: false,
message: {},
email: '',
showPassword: false,
showPasswordConfirmation: false,
};
Expand All @@ -52,21 +53,59 @@ class PasswordReset extends Component {
*
* @return {undefined}
*/
handleSubmit = async (values, { setSubmitting }) => {
handleSubmit = async (values, { setSubmitting, setErrors }) => {
setSubmitting(false);

this.setState({ loading: true });

try {
const { match, pageProps } = this.props;
const { token } = match.params;

const response = await axios.patch(
`api/v1/auth/password/reset/${token}`,
values,
);

await pageProps.authenticate(JSON.stringify(response.data));

this.setState({ loading: false });
} catch (error) {
if (!error.response) {
throw new Error('Unknown error');
}

const { errors } = error.response.data;

if (errors) {
setErrors(errors);
}

this.setState({ loading: false });
}
};

componentDidMount() {
const { location } = this.props;

const queryParams = UrlUtils._queryParams(location.search);

if (!queryParams.hasOwnProperty('email')) {
return;
}

this.setState({
email: queryParams.email,
});
}

render() {
const { classes, location } = this.props;
const email = UrlUtils._queryParams(location.search).hasOwnProperty(
'email',
)
? UrlUtils._queryParams(location.search).email
: '';
const { classes } = this.props;

const {
loading,
message,
email,
showPassword,
showPasswordConfirmation,
} = this.state;
Expand Down Expand Up @@ -265,4 +304,4 @@ const styles = theme => ({
},
});

export default withStyles(styles)(withFormik({})(PasswordReset));
export default withStyles(styles)(PasswordReset);
1 change: 1 addition & 0 deletions resources/lang/en/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
'not_regex' => 'The :attribute format is invalid.',
'numeric' => 'The :attribute must be a number.',
'present' => 'The :attribute field must be present.',
'pwned' => 'The :attribute is weak, please enter a strong password.',
'regex' => 'The :attribute format is invalid.',
'required' => 'The :attribute field is required.',
'required_if' => 'The :attribute field is required when :other is :value.',
Expand Down
1 change: 1 addition & 0 deletions resources/lang/fil/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
'not_regex' => 'The :attribute format is invalid.',
'numeric' => 'The :attribute must be a number.',
'present' => 'The :attribute field must be present.',
'pwned' => 'The :attribute is weak, please enter a strong password.',
'regex' => 'The :attribute format is invalid.',
'required' => 'The :attribute field is required.',
'required_if' => 'The :attribute field is required when :other is :value.',
Expand Down
2 changes: 1 addition & 1 deletion routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

Route::name('password.')->prefix('password')->group(function () {
Route::post('request', 'ForgotPasswordController@sendResetLinkEmail')->name('request');
Route::post('reset/{token}', 'ResetPasswordController@reset')->name('reset');
Route::patch('reset/{token}', 'ResetPasswordController@reset')->name('reset');
});
});

Expand Down
23 changes: 22 additions & 1 deletion tests/Feature/Api/V1/Auth/ForgotPasswordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,30 @@
namespace Tests\Feature\Api\V1\Auth;

use App\User;
use App\Jobs\ProcessPasswordResetRequest;
use Illuminate\Support\Facades\Bus;
use Tests\Feature\Api\V1\BaseTest;

class ForgotPasswordTest extends BaseTest
{
/** @test */
public function a_user_can_request_for_password_reset_link()
{
// The user that is requesting for the Password Reset Link.
$user = User::first();

}
// The response body that should be sent alongside the request.
$body = [
'email' => $user->email
];

// Assuming that the Password Reset Request is processed,
// It must return a 200 response status and then,
// It must return a response body containing: `Sending...`.
$this->post(route('api.v1.auth.password.request'), $body)
->assertStatus(200)
->assertSee('Sending...');

// TODO: Assert if Jobs are dispatched & Notifications are sent.
}
}
Loading

0 comments on commit 9b02283

Please sign in to comment.