Skip to content

Commit

Permalink
feat!(password): separate update password functionality from `updat…
Browse files Browse the repository at this point in the history
…e user` functionality
  • Loading branch information
Mohammad-Alavi committed Apr 20, 2022
1 parent 0cb1058 commit d3a57a2
Show file tree
Hide file tree
Showing 11 changed files with 324 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

namespace App\Containers\AppSection\Authentication\UI\API\Requests;

use App\Containers\AppSection\User\Models\User;
use App\Ship\Parents\Requests\Request;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;

class RegisterUserRequest extends Request
{
Expand Down Expand Up @@ -37,11 +37,7 @@ public function rules(): array
'email' => 'required|email|unique:users,email',
'password' => [
'required',
Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols(),
User::getPasswordValidationRules(),
],
'name' => 'min:2|max:50',
'gender' => 'in:male,female,unspecified',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace App\Containers\AppSection\User\Actions;

use Apiato\Core\Exceptions\IncorrectIdException;
use App\Containers\AppSection\User\Models\User;
use App\Containers\AppSection\User\Tasks\UpdateUserTask;
use App\Containers\AppSection\User\UI\API\Requests\UpdateUserPasswordRequest;
use App\Ship\Exceptions\NotFoundException;
use App\Ship\Exceptions\UpdateResourceFailedException;
use App\Ship\Parents\Actions\Action;

class UpdateUserPasswordAction extends Action
{
/**
* @param UpdateUserPasswordRequest $request
* @return User
* @throws IncorrectIdException
* @throws NotFoundException
* @throws UpdateResourceFailedException
*/
public function run(UpdateUserPasswordRequest $request): User
{
$sanitizedData = $request->sanitizeInput([
'current_password',
'new_password',
]);

return app(UpdateUserTask::class)->run(['password' => $sanitizedData['new_password']], $request->id);
}
}
10 changes: 10 additions & 0 deletions app/Containers/AppSection/User/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use App\Ship\Parents\Models\UserModel;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Notifications\Notifiable;
use Illuminate\Validation\Rules\Password;

class User extends UserModel implements MustVerifyEmail
{
Expand All @@ -34,6 +35,15 @@ class User extends UserModel implements MustVerifyEmail
'birth' => 'date',
];

public static function getPasswordValidationRules(): Password
{
return Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols();
}

public function sendEmailVerificationNotificationWithVerificationUrl(string $verificationUrl)
{
$this->notify(new VerifyEmail($verificationUrl));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace App\Containers\AppSection\User\UI\API\Controllers;

use Apiato\Core\Exceptions\IncorrectIdException;
use Apiato\Core\Exceptions\InvalidTransformerException;
use App\Containers\AppSection\User\Actions\UpdateUserPasswordAction;
use App\Containers\AppSection\User\UI\API\Requests\UpdateUserPasswordRequest;
use App\Containers\AppSection\User\UI\API\Transformers\UserTransformer;
use App\Ship\Exceptions\NotFoundException;
use App\Ship\Exceptions\UpdateResourceFailedException;
use App\Ship\Parents\Controllers\ApiController;

class UpdateUserPasswordController extends ApiController
{
/**
* @param UpdateUserPasswordRequest $request
* @return array
* @throws IncorrectIdException
* @throws InvalidTransformerException
* @throws NotFoundException
* @throws UpdateResourceFailedException
*/
public function updateUserPassword(UpdateUserPasswordRequest $request): array
{
$user = app(UpdateUserPasswordAction::class)->run($request);

return $this->transform($user, UserTransformer::class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace App\Containers\AppSection\User\UI\API\Requests;

use App\Containers\AppSection\Authorization\Traits\IsResourceOwnerTrait;
use App\Containers\AppSection\User\Models\User;
use App\Ship\Parents\Requests\Request;
use Illuminate\Validation\Rule;

class UpdateUserPasswordRequest extends Request
{
use IsResourceOwnerTrait;

/**
* Define which Roles and/or Permissions has access to this request.
*/
protected array $access = [
'permissions' => '',
'roles' => '',
];

/**
* Id's that needs decoding before applying the validation rules.
*/
protected array $decode = [
'id',
];

/**
* Defining the URL parameters (`/stores/999/items`) allows applying
* validation rules on them and allows accessing them like request data.
*/
protected array $urlParameters = [
'id',
];

public function rules(): array
{
return [
'current_password' => [Rule::requiredIf(
fn (): bool => !is_null($this->user()->password)
), 'current_password:api'],
'new_password' => [
'required',
User::getPasswordValidationRules(),
],
];
}

public function authorize(): bool
{
return $this->check([
'hasAccess|isResourceOwner',
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

namespace App\Containers\AppSection\User\UI\API\Requests;

use App\Ship\Parents\Requests\Request;
use App\Containers\AppSection\Authorization\Traits\IsResourceOwnerTrait;
use App\Ship\Parents\Requests\Request;

class UpdateUserRequest extends Request
{
Expand Down Expand Up @@ -35,7 +35,6 @@ class UpdateUserRequest extends Request
public function rules(): array
{
return [
'password' => 'min:6|max:40',
'name' => 'min:2|max:50',
'gender' => 'in:male,female,unspecified',
'birth' => 'date',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
* @apiVersion 1.0.0
* @apiPermission Authenticated ['permissions' => 'update-users', 'roles' => ''] | Resource Owner
*
* @apiParam {String} [password] min:6|max:40
* @apiParam {String} [name] min:2|max:50
* @apiParam {String="male","female","unspecified"} [gender]
* @apiParam {Date} [birth] format: Y-m-d / e.g. 2015-10-15
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/**
* @apiGroup User
* @apiName UpdateUserPassword
* @api {patch} /v1/users/:id Update User's Password
*
* @apiVersion 1.0.0
* @apiPermission Authenticated ['permissions' => 'update-users', 'roles' => ''] | Resource Owner
*
* @apiParam {String} current_password
* @apiParam {String} new_password min: 8
*
* at least one character of the following:
*
* upper case letter
*
* lower case letter
*
* number
*
* special character
*
* @apiUse UserSuccessSingleResponse
*/

use App\Containers\AppSection\User\UI\API\Controllers\UpdateUserPasswordController;
use Illuminate\Support\Facades\Route;

Route::patch('users/{id}/password', [UpdateUserPasswordController::class, 'updateUserPassword'])
->middleware(['auth:api']);
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

namespace App\Containers\AppSection\User\UI\API\Tests\Functional;

use App\Containers\AppSection\User\UI\API\Tests\ApiTestCase;
use Illuminate\Testing\Fluent\AssertableJson;

/**
* Class UpdateUserPasswordTest.
*
* @group user
* @group api
*/
class UpdateUserPasswordTest extends ApiTestCase
{
protected string $endpoint = 'patch@v1/users/{id}/password';

protected array $access = [
'permissions' => '',
'roles' => '',
];

public function testGivenUserAlreadyHaveAPassword_UpdateUserPassword(): void
{
$user = $this->getTestingUser([
'password' => 'Av@dakedavra!',
]);
$data = [
'current_password' => 'Av@dakedavra!',
'new_password' => 'updated#Password111',
];

$response = $this->injectId($user->id)->makeCall($data);

$response->assertStatus(200);
$response->assertJson(
fn (AssertableJson $json) => $json->has('data')
->where('data.object', 'User')
->where('data.email', $user->email)
->missing('data.password')
->etc()
);
}

public function testNewPasswordFieldShouldBeRequired(): void
{
$user = $this->getTestingUser();
$data = [
'new_password' => '',
];

$response = $this->injectId($user->id)->makeCall($data);

$response->assertStatus(422);
$response->assertJson(
fn (AssertableJson $json) => $json->has('errors')
->where('errors.new_password.0', 'The new password field is required.')
->etc()
);
}

public function testGivenUserAlreadyHaveAPassword_CurrentPasswordFieldShouldBeRequired(): void
{
$user = $this->getTestingUser([
'password' => 'Av@dakedavra!',
]);
$data = [
'new_password' => 'updated#Password111',
];

$response = $this->injectId($user->id)->makeCall($data);

$response->assertStatus(422);
$response->assertJson(
fn (AssertableJson $json) => $json->has('errors')
->where('errors.current_password.0', 'The current password field is required.')
->etc()
);
}

public function testGivenUserAlreadyHaveAPassword_CurrentPasswordFieldMustMatchUserCurrentPassword(): void
{
$user = $this->getTestingUser([
'password' => 'Av@dakedavra!',
]);
$data = [
'current_password' => 'notMatchingP@ssw0rd',
'new_password' => 'updated#Password111',
];

$response = $this->injectId($user->id)->makeCall($data);

$response->assertStatus(422);
$response->assertJson(
fn (AssertableJson $json) => $json->has('errors')
->where('errors.current_password.0', 'The password is incorrect.')
->etc()
);
}

public function testGivenUserDoesntHaveAPassword_CurrentPasswordFieldShouldBeProhibited(): void
{
$user = $this->getTestingUser([
'password' => null,
]);
$data = [
'current_password' => 'sh0uldBeProhibited!!11',
'new_password' => 'updated#Password111',
];

$response = $this->injectId($user->id)->makeCall($data);

$response->assertStatus(422);
$response->assertJson(
fn (AssertableJson $json) => $json->has('errors')
->where('errors.current_password.0', 'The password is incorrect.')
->etc()
);
}
}
Loading

0 comments on commit d3a57a2

Please sign in to comment.