diff --git a/composer.json b/composer.json index c531237c7..9134d597d 100644 --- a/composer.json +++ b/composer.json @@ -20,8 +20,9 @@ "require": { "php": "^7.4", "ext-json": "*", + "doctrine/dbal": "^2.10", "illuminate/support": "^6.0|^7.0", - "doctrine/dbal": "^2.10" + "laravel/ui": "^2.0" }, "require-dev": { "mockery/mockery": "^1.3", diff --git a/config/config.php b/config/config.php index 966bd7a06..8c10001d1 100644 --- a/config/config.php +++ b/config/config.php @@ -25,10 +25,24 @@ | this will be used for the verification of the authenticatable model and provide the | authorizable functionality | - | Supported: "passport", "airlock" + | Supported: "passport", "sanctum" */ - 'provider' => 'airlock', + 'provider' => 'sanctum', + + /* + |-------------------------------------------------------------------------- + | Auth frontend app url + |-------------------------------------------------------------------------- + | + |URL used for reset password URL generating. + | + | + */ + + 'frontend_app_url' => env('FRONTEND_APP_URL', env('APP_URL')), + + 'password_reset_url' => env('FRONTEND_APP_URL').'/password/reset?token={token}&email={email}', ], /* diff --git a/src/Commands/CheckPassport.php b/src/Commands/CheckPassport.php index 37eb69ae1..5492e8b50 100644 --- a/src/Commands/CheckPassport.php +++ b/src/Commands/CheckPassport.php @@ -148,7 +148,7 @@ public function hasPassportClient(): bool } catch (\RuntimeException $e) { $this->warn($e->getMessage()); $this->warn('Hint: php artisan passport:client --personal'); - $this->warn('See: https://laravel.com/docs/6.x/passport#creating-a-personal-access-client'); + $this->warn('See: https://laravel.com/docs/7.x/passport#creating-a-personal-access-client'); return false; } diff --git a/src/Contracts/Sanctumable.php b/src/Contracts/Sanctumable.php new file mode 100644 index 000000000..9ec0ebf3c --- /dev/null +++ b/src/Contracts/Sanctumable.php @@ -0,0 +1,7 @@ +authService = $authService; + } + + public function login(Request $request) + { + return $this->authService->login($request); + } + + public function register(Request $request) + { + return $this->authService->register($request); + } + + public function verify(Request $request) + { + return $this->authService->verify($request); + } + + public function forgotPassword(Request $request) + { + return $this->authService->forgotPassword($request); + } + + public function resetPassword(Request $request) + { + return $this->authService->resetPassword($request); + } +} diff --git a/src/Exceptions/AirlockUserException.php b/src/Exceptions/AirlockUserException.php deleted file mode 100644 index 7160ef950..000000000 --- a/src/Exceptions/AirlockUserException.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ -class AirlockUserException extends Exception -{ -} diff --git a/src/Exceptions/AuthenticatableUserException.php b/src/Exceptions/AuthenticatableUserException.php index e2222ae2a..5f61fde0c 100644 --- a/src/Exceptions/AuthenticatableUserException.php +++ b/src/Exceptions/AuthenticatableUserException.php @@ -9,4 +9,10 @@ */ class AuthenticatableUserException extends Exception { + public static function wrongInstance(): self + { + $message = __("Repository model should be an instance of \Illuminate\Contracts\Auth\Authenticatable"); + + return new static($message); + } } diff --git a/src/Exceptions/SanctumUserException.php b/src/Exceptions/SanctumUserException.php new file mode 100644 index 000000000..ac97e7280 --- /dev/null +++ b/src/Exceptions/SanctumUserException.php @@ -0,0 +1,16 @@ + + */ +class SanctumUserException extends Exception +{ + public static function wrongConfiguration() + { + return new static('Auth provider should be [sanctum] in the configuration [restify.auth.provider].'); + } +} diff --git a/src/Notifications/VerifyEmail.php b/src/Notifications/VerifyEmail.php new file mode 100644 index 000000000..a3397258b --- /dev/null +++ b/src/Notifications/VerifyEmail.php @@ -0,0 +1,58 @@ +verificationUrl($notifiable); + + if (static::$toMailCallback) { + return call_user_func(static::$toMailCallback, $notifiable, $verificationUrl); + } + + return (new MailMessage) + ->subject(Lang::get('Verify Email Address')) + ->line(Lang::get('Please click the button below to verify your email address.')) + ->action(Lang::get('Verify Email Address'), $verificationUrl) + ->line(Lang::get('If you did not create an account, no further action is required.')); + } + + /** + * Get the verification URL for the given notifiable. + * + * @param mixed $notifiable + * @return string + */ + protected function verificationUrl($notifiable) + { + return URL::temporarySignedRoute( + 'restify.verification.verify', + Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)), + [ + 'id' => $notifiable->getKey(), + 'hash' => sha1($notifiable->getEmailForVerification()), + ] + ); + } + + /** + * Set a callback that should be used when building the notification mail message. + * + * @param \Closure $callback + * @return void + */ + public static function toMailUsing($callback) + { + static::$toMailCallback = $callback; + } +} diff --git a/src/Services/AuthService.php b/src/Services/AuthService.php index dfed14ad9..0baf82cfc 100644 --- a/src/Services/AuthService.php +++ b/src/Services/AuthService.php @@ -2,26 +2,12 @@ namespace Binaryk\LaravelRestify\Services; -use Binaryk\LaravelRestify\Contracts\Airlockable; use Binaryk\LaravelRestify\Contracts\Passportable; -use Binaryk\LaravelRestify\Events\UserLoggedIn; -use Binaryk\LaravelRestify\Events\UserLogout; -use Binaryk\LaravelRestify\Exceptions\AirlockUserException; -use Binaryk\LaravelRestify\Exceptions\AuthenticatableUserException; -use Binaryk\LaravelRestify\Exceptions\CredentialsDoesntMatch; +use Binaryk\LaravelRestify\Contracts\Sanctumable; use Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException; use Binaryk\LaravelRestify\Exceptions\PassportUserException; -use Binaryk\LaravelRestify\Exceptions\PasswordResetException; -use Binaryk\LaravelRestify\Exceptions\PasswordResetInvalidTokenException; -use Binaryk\LaravelRestify\Exceptions\UnverifiedUser; -use Binaryk\LaravelRestify\Http\Requests\ResetPasswordRequest; -use Binaryk\LaravelRestify\Http\Requests\RestifyPasswordEmailRequest; -use Binaryk\LaravelRestify\Http\Requests\RestifyRegisterRequest; -use Binaryk\LaravelRestify\Tests\Fixtures\User; -use Closure; +use Binaryk\LaravelRestify\Exceptions\SanctumUserException; use Illuminate\Auth\Access\AuthorizationException; -use Illuminate\Auth\Events\PasswordReset; -use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Events\Verified; use Illuminate\Container\Container; use Illuminate\Contracts\Auth\Authenticatable; @@ -29,13 +15,9 @@ use Illuminate\Contracts\Auth\PasswordBroker; use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Facades\Auth; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Config; -use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Password; -use Illuminate\Support\Facades\Validator; -use Illuminate\Support\Str; -use Illuminate\Validation\ValidationException; use ReflectionException; /** @@ -43,88 +25,28 @@ */ class AuthService extends RestifyService { - /** - * @var string - */ - public static $registerFormRequest = RestifyRegisterRequest::class; - - /** - * The callback that should be used to create the registered user. - * - * @var Closure|null - */ - public static $creating; - - /** - * @param array $credentials - * @return string|null - * @throws CredentialsDoesntMatch - * @throws UnverifiedUser - * @throws PassportUserException - * @throws AirlockUserException - */ - public function login(array $credentials = []) + public function login(Request $request) { - $token = null; - - if (Auth::attempt($credentials) === false) { - throw new CredentialsDoesntMatch("Credentials doesn't match"); + if (config('restify.auth.provider') !== 'sanctum') { + throw SanctumUserException::wrongConfiguration(); } - /** - * @var Authenticatable|Passportable|Airlockable - */ - $user = Auth::user(); - - if ($user instanceof MustVerifyEmail && $user->hasVerifiedEmail() === false) { - throw new UnverifiedUser('The email is not verified'); - } - - $this->validateUserModel($user); - - if (method_exists($user, 'createToken')) { - $token = $user->createToken('Login')->accessToken; - event(new UserLoggedIn($user)); - } + $token = LoginService::make($request); return $token; } - /** - * @param array $payload - * @return \Illuminate\Database\Eloquent\Builder|Model|mixed - * @throws AuthenticatableUserException - * @throws EntityNotFoundException - * @throws PassportUserException - * @throws ValidationException - * @throws BindingResolutionException - * @throws AirlockUserException - */ - public function register(array $payload) + public function register(Request $request) { - $this->validateRegister($payload); - - $builder = $this->userQuery(); - - if (false === $builder instanceof Authenticatable) { - throw new AuthenticatableUserException(__("Repository model should be an instance of \Illuminate\Contracts\Auth\Authenticatable")); - } - - /** - * @var Authenticatable - */ - $user = $builder->query()->create(array_merge($payload, [ - 'password' => Hash::make(data_get($payload, 'password')), - ])); - - if ($user instanceof Authenticatable) { - event(new Registered($user)); - } + return RegisterService::make($request, $this); + } - return $user; + public function forgotPassword(Request $request) + { + return ForgotPasswordService::make($request); } - /** + /* * @param $id * @param null $hash * @return Builder|Builder[]|\Illuminate\Database\Eloquent\Collection|Model|null @@ -139,7 +61,7 @@ public function verify($id, $hash = null) */ $user = $this->userQuery()->query()->find($id); - if ($user instanceof Passportable && ! hash_equals((string) $hash, sha1($user->getEmail()))) { + if ($user instanceof Sanctumable && ! hash_equals((string) $hash, sha1($user->getEmailForVerification()))) { throw new AuthorizationException('Invalid hash'); } @@ -150,63 +72,9 @@ public function verify($id, $hash = null) return $user; } - /** - * @param $email - * @return string - * @throws EntityNotFoundException - * @throws PasswordResetInvalidTokenException - * @throws ValidationException - * @throws PasswordResetException - */ - public function sendResetPasswordLinkEmail($email) + public function resetPassword(Request $request) { - $validator = Validator::make(compact('email'), (new RestifyPasswordEmailRequest)->rules(), (new RestifyPasswordEmailRequest)->messages()); - if ($validator->fails()) { - // this is manually thrown for readability - throw new ValidationException($validator); - } - // We will send the password reset link to this user. Once we have attempted - // to send the link, we will examine the response then see the message we - // need to show to the user. Finally, we'll send out a proper response. - $response = $this->broker()->sendResetLink(compact('email')); - $this->resolveBrokerResponse($response, PasswordBroker::RESET_LINK_SENT, PasswordBroker::PASSWORD_RESET); - - return $response; - } - - /** - * @param array $credentials - * @return JsonResponse - * @throws PasswordResetInvalidTokenException - * @throws ValidationException - * @throws EntityNotFoundException - * @throws PasswordResetException - */ - public function resetPassword(array $credentials = []) - { - $validator = Validator::make($credentials, (new ResetPasswordRequest())->rules(), (new ResetPasswordRequest())->messages()); - if ($validator->fails()) { - // this is manually thrown for readability - throw new ValidationException($validator); - } - - // Here we will attempt to reset the user's password. If it is successful we - // will update the password on an actual user model and persist it to the - // database. Otherwise we will parse the error and return the response. - $response = $this->broker()->reset( - $credentials, function ($user, $password) { - $user->password = Hash::make($password); - - $user->setRememberToken(Str::random(60)); - - $user->save(); - - event(new PasswordReset($user)); - }); - - $this->resolveBrokerResponse($response, PasswordBroker::PASSWORD_RESET); - - return $response; + return ResetPasswordService::make($request, $this); } /** @@ -220,10 +88,10 @@ public function broker() /** * Returns query for User model and validate if it exists. * - * @throws EntityNotFoundException - * @throws PassportUserException - * @throws AirlockUserException * @return Model + * @throws PassportUserException + * @throws SanctumUserException + * @throws EntityNotFoundException */ public function userQuery() { @@ -244,7 +112,7 @@ public function userQuery() /** * @param $userInstance * @throws PassportUserException - * @throws AirlockUserException + * @throws SanctumUserException */ public function validateUserModel($userInstance) { @@ -252,78 +120,13 @@ public function validateUserModel($userInstance) throw new PassportUserException(__("User is not implementing Binaryk\LaravelRestify\Contracts\Passportable contract. User can use 'Laravel\Passport\HasApiTokens' trait")); } - if (config('restify.auth.provider') === 'airlock' && false === $userInstance instanceof Airlockable) { - throw new AirlockUserException(__("User is not implementing Binaryk\LaravelRestify\Contracts\Airlockable contract. User should use 'Laravel\Airlock\HasApiTokens' trait to provide")); + if (config('restify.auth.provider') === 'sanctum' && false === $userInstance instanceof Sanctumable) { + throw new SanctumUserException(__("User is not implementing Binaryk\LaravelRestify\Contracts\Sanctumable contract. User should use 'Laravel\Sanctum\HasApiTokens' trait to provide")); } } - /** - * @param $response - * @param null $case - * @throws EntityNotFoundException - * @throws PasswordResetException - * @throws PasswordResetInvalidTokenException - */ - protected function resolveBrokerResponse($response, $case = null) - { - if ($response === PasswordBroker::INVALID_TOKEN) { - throw new PasswordResetInvalidTokenException(__('Invalid token.')); - } - - if ($response === PasswordBroker::INVALID_USER) { - throw new EntityNotFoundException(__("User with provided email doesn't exists.")); - } - if ($case && $response !== $case) { - throw new PasswordResetException($response); - } - } - - /** - * @param array $payload - * @return bool - * @throws ValidationException - * @throws BindingResolutionException - */ - public function validateRegister(array $payload) - { - try { - if (class_exists(static::$registerFormRequest) && (new \ReflectionClass(static::$registerFormRequest))->isInstantiable()) { - $validator = Validator::make($payload, (new static::$registerFormRequest)->rules(), (new static::$registerFormRequest)->messages()); - if ($validator->fails()) { - throw new ValidationException($validator); - } - } - } catch (ReflectionException $e) { - $concrete = static::$registerFormRequest; - throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e); - } - - return true; - } - - /** - * Revoke tokens for user. - * - * @throws AuthenticatableUserException - */ public function logout() { - /** - * @var User - */ - $user = Auth::user(); - if ($user instanceof Authenticatable) { - if ($user instanceof Passportable) { - $user->tokens()->get()->each->revoke(); - event(new UserLogout($user)); - } - - if ($user instanceof Airlockable) { - $user->tokens->each->delete(); - event(new UserLogout($user)); - } - } else { - throw new AuthenticatableUserException(__('User is not authenticated.')); - } + return LogoutService::make(); } } diff --git a/src/Services/ForgotPasswordService.php b/src/Services/ForgotPasswordService.php new file mode 100644 index 000000000..f04668ce6 --- /dev/null +++ b/src/Services/ForgotPasswordService.php @@ -0,0 +1,27 @@ + + */ +class ForgotPasswordService +{ + use SendsPasswordResetEmails; + + public static function make(Request $request) + { + ResetPassword::createUrlUsing(function ($notifiable, $token) { + $withToken = str_replace(['{token}'], $token, config('restify.auth.password_reset_url')); + $withEmail = str_replace(['{email}'], $notifiable->getEmailForPasswordReset(), $withToken); + + return url($withEmail); + }); + + return resolve(static::class)->sendResetLinkEmail($request); + } +} diff --git a/src/Services/LoginService.php b/src/Services/LoginService.php new file mode 100644 index 000000000..909b77e18 --- /dev/null +++ b/src/Services/LoginService.php @@ -0,0 +1,48 @@ + + */ +class LoginService +{ + use AuthenticatesUsers; + + public static function make(Request $request) + { + return resolve(static::class)->login($request); + } + + public function login(Request $request) + { + $this->validateLogin($request); + + // If the class is using the ThrottlesLogins trait, we can automatically throttle + // the login attempts for this application. We'll key this by the username and + // the IP address of the client making these requests into this application. + if (method_exists($this, 'hasTooManyLoginAttempts') && + $this->hasTooManyLoginAttempts($request)) { + $this->fireLockoutEvent($request); + + return $this->sendLockoutResponse($request); + } + + if ($this->attemptLogin($request)) { + event(new UserLoggedIn($this->guard()->user())); + + return $this->guard()->user()->createToken('login'); + } + + // If the login attempt was unsuccessful we will increment the number of attempts + // to login and redirect the user back to the login form. Of course, when this + // user surpasses their maximum number of attempts they will get locked out. + $this->incrementLoginAttempts($request); + + return $this->sendFailedLoginResponse($request); + } +} diff --git a/src/Services/LogoutService.php b/src/Services/LogoutService.php new file mode 100644 index 000000000..72b36a8bb --- /dev/null +++ b/src/Services/LogoutService.php @@ -0,0 +1,32 @@ + + */ +class LogoutService +{ + use AuthenticatesUsers; + + public static function make(Request $request) + { + /** + * @var User + */ + $user = Auth::user(); + + if ($user instanceof Authenticatable) { + return resolve(static::class)->logout($request); + } else { + throw new AuthenticatableUserException(__('User is not authenticated.')); + } + } +} diff --git a/src/Services/PassportService.php b/src/Services/PassportService.php new file mode 100644 index 000000000..dbe2fd5b6 --- /dev/null +++ b/src/Services/PassportService.php @@ -0,0 +1,91 @@ + + */ +class PassportService extends RestifyService +{ + /** + * Create user token based on credentials. + * + * @param array $credentials + * @return string|null + * @throws CredentialsDoesntMatch + * @throws PassportUserException + * @throws UnverifiedUser + */ + public function createToken(array $credentials = []) + { + $token = null; + + if (Auth::attempt($credentials) === false) { + throw new CredentialsDoesntMatch("Credentials doesn't match"); + } + + /** + * @var Authenticatable|Passportable|Sanctumable + */ + $user = Auth::user(); + + if ($user instanceof MustVerifyEmail && $user->hasVerifiedEmail() === false) { + throw new UnverifiedUser('The email is not verified'); + } + + $this->validateUserModel($user); + + if (method_exists($user, 'createToken')) { + $token = $user->createToken('Login'); + event(new UserLoggedIn($user)); + } + + return $token; + } + + /** + * @param $userInstance + * @throws PassportUserException + */ + public function validateUserModel($userInstance) + { + if (config('restify.auth.provider') === 'passport' && false === $userInstance instanceof Passportable) { + throw new PassportUserException(__("User is not implementing Binaryk\LaravelRestify\Contracts\Passportable contract. User can use 'Laravel\Passport\HasApiTokens' trait")); + } + } + + /** + * Revoke tokens for user. + * + * @throws AuthenticatableUserException + */ + public function logout() + { + /** + * @var User + */ + $user = Auth::user(); + + if ($user instanceof Authenticatable) { + if ($user instanceof Passportable) { + $user->tokens()->get()->each->revoke(); + event(new UserLogout($user)); + } + } else { + throw new AuthenticatableUserException(__('User is not authenticated.')); + } + } +} diff --git a/src/Services/RegisterService.php b/src/Services/RegisterService.php new file mode 100644 index 000000000..1681c078a --- /dev/null +++ b/src/Services/RegisterService.php @@ -0,0 +1,95 @@ + + */ +class RegisterService +{ + /** + * @var AuthService + */ + protected $authService; + + /** + * The callback that should be used to create the registered user. + * + * @var Closure|null + */ + public static $creating; + + public static $registerFormRequest = RestifyRegisterRequest::class; + + public function register(Request $request) + { + $payload = $request->all(); + + $this->validateRegister($payload); + + $builder = $this->authService->userQuery(); + + if (false === $builder instanceof Authenticatable) { + throw AuthenticatableUserException::wrongInstance(); + } + + $userData = array_merge($payload, [ + 'password' => Hash::make(data_get($payload, 'password')), + ]); + + if (is_callable(static::$creating)) { + $user = call_user_func(static::$creating, $userData); + } else { + $user = $builder->query()->create($userData); + } + + if ($user instanceof Authenticatable) { + event(new Registered($user)); + } + + return $user; + } + + public static function make(Request $request, AuthService $authService) + { + return resolve(static::class) + ->usingAuthService($authService) + ->register($request); + } + + public function validateRegister(array $payload) + { + try { + if (class_exists(static::$registerFormRequest) && (new \ReflectionClass(static::$registerFormRequest))->isInstantiable()) { + $validator = Validator::make($payload, (new static::$registerFormRequest)->rules(), (new static::$registerFormRequest)->messages()); + if ($validator->fails()) { + throw new ValidationException($validator); + } + } + } catch (ReflectionException $e) { + $concrete = static::$registerFormRequest; + throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e); + } + + return true; + } + + protected function usingAuthService(AuthService $service) + { + $this->authService = $service; + + return $this; + } +} diff --git a/src/Services/ResetPasswordService.php b/src/Services/ResetPasswordService.php new file mode 100644 index 000000000..3597fe379 --- /dev/null +++ b/src/Services/ResetPasswordService.php @@ -0,0 +1,29 @@ + + */ +class ResetPasswordService +{ + use ResetsPasswords; + + protected $authService; + + public static function make(Request $request, AuthService $authService) + { + return resolve(static::class) + ->reset($request); + } + + protected function usingAuthService(AuthService $authService) + { + $this->authService = $authService; + + return $this; + } +} diff --git a/tests/Feature/Authentication/AuthServiceForgotPasswordTest.php b/tests/Feature/Authentication/AuthServiceForgotPasswordTest.php index 763ea23f3..eabb4fb3a 100644 --- a/tests/Feature/Authentication/AuthServiceForgotPasswordTest.php +++ b/tests/Feature/Authentication/AuthServiceForgotPasswordTest.php @@ -3,16 +3,18 @@ namespace Binaryk\LaravelRestify\Tests\Feature\Authentication; use Binaryk\LaravelRestify\Events\UserLoggedIn; -use Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException; -use Binaryk\LaravelRestify\Exceptions\PasswordResetInvalidTokenException; use Binaryk\LaravelRestify\Services\AuthService; +use Binaryk\LaravelRestify\Services\RegisterService; use Binaryk\LaravelRestify\Tests\Fixtures\MailTracking; use Binaryk\LaravelRestify\Tests\Fixtures\User\User; use Binaryk\LaravelRestify\Tests\IntegrationTest; use Illuminate\Auth\Events\PasswordReset; use Illuminate\Auth\Events\Registered; +use Illuminate\Auth\Notifications\ResetPassword; use Illuminate\Foundation\Testing\Concerns\InteractsWithContainer; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Notification; use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; @@ -33,100 +35,90 @@ protected function setUp(): void { parent::setUp(); $this->setUpMailTracking(); - AuthService::$registerFormRequest = null; + RegisterService::$registerFormRequest = null; $this->authService = resolve(AuthService::class); - } - - public function test_invalid_email_in_payload() - { - $this->expectException(ValidationException::class); - $this->authService->sendResetPasswordLinkEmail('invalid_email'); + $this->app['config']->set('restify.auth.provider', 'sanctum'); + $this->app['config']->set('restify.auth.frontend_app_url', 'https://laravel-restify.dev'); + $this->app['config']->set('restify.auth.password_reset_url', 'https://laravel-restify.dev/password/reset?token={token}&email={email}'); } public function test_email_was_sent_and_contain_token() { + Notification::fake(); + $user = $this->register(); - $this->authService->sendResetPasswordLinkEmail($user->email); - if ($this->lastEmail()) { - $lastEmail = $this->lastEmail()->getBody(); - preg_match_all('/token=([\w\.]*)/i', $lastEmail, $data); - $token = $data[1][0]; - - $this->assertEmailsSent(1); - $this->assertEmailTo($user->email); - $this->assertNotNull($token); - } + $request = new Request([], []); + $request->merge(['email' => $user->email]); + + $this->authService->forgotPassword($request); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) { + $this->assertNotEmpty($notification->token); + + return true; + }); } public function test_reset_password_invalid_payload() { $this->expectException(ValidationException::class); - $this->authService->resetPassword([ + $request = new Request([], []); + $request->merge([ 'email' => null, 'password' => 'password', 'password_confirmation' => 'password', 'token' => 'secret', ]); - } - - public function test_reset_password_invalid_token() - { - $user = $this->register(); - $this->expectException(PasswordResetInvalidTokenException::class); - $this->authService->resetPassword([ - 'email' => $user->email, - 'password' => 'password', - 'password_confirmation' => 'password', - 'token' => 'secret', - ]); - } - - public function test_reset_password_invalid_user() - { - $this->expectException(EntityNotFoundException::class); - $this->authService->resetPassword([ - 'email' => 'random@test.com', - 'password' => 'password', - 'password_confirmation' => 'password', - 'token' => 'secret', - ]); + $this->authService->resetPassword($request); } public function test_reset_password_successfully() { + Notification::fake(); $user = $this->register(); $this->authService->verify($user->id, sha1($user->email)); - $this->authService->sendResetPasswordLinkEmail($user->email); - if ($this->lastEmail()) { - $lastEmail = $this->lastEmail()->getBody(); - preg_match_all('/token=([\w\.]*)/i', $lastEmail, $data); - $token = $data[1][0]; + + $request = new Request([], []); + $request->merge(['email' => $user->email]); + + $this->authService->forgotPassword($request); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { + $token = $notification->token; $password = Str::random(10); - $this->authService->resetPassword([ + $request = new Request([], []); + $request->merge([ 'email' => $user->email, 'password' => $password, 'password_confirmation' => $password, 'token' => $token, ]); + $this->authService->resetPassword($request); + Event::assertDispatched(PasswordReset::class, function ($e) use ($user) { $this->assertEquals($e->user->email, $user->email); return $e->user instanceof User; }); - $this->authService->login([ + $request = new Request([], []); + $request->merge([ 'email' => $user->email, 'password' => $password, ]); + $this->authService->login($request); + Event::assertDispatched(UserLoggedIn::class, function ($e) use ($user) { $this->assertEquals($e->user->email, $user->email); return $e->user instanceof User; }); - } + + return true; + }); } public function register() @@ -139,14 +131,18 @@ public function register() $this->app->instance(User::class, new User); + $request = new Request([], []); + $user = [ 'name' => 'Eduard Lupacescu', 'email' => 'eduard.lupacescu@binarcode.com', - 'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', + 'password' => 'secret!', 'remember_token' => Str::random(10), ]; - $this->authService->register($user); + $request->merge($user); + + $this->authService->register($request); return User::query()->get()->last(); } diff --git a/tests/Feature/Authentication/AuthServiceLoginTest.php b/tests/Feature/Authentication/AuthServiceLoginTest.php deleted file mode 100644 index 568247ed5..000000000 --- a/tests/Feature/Authentication/AuthServiceLoginTest.php +++ /dev/null @@ -1,190 +0,0 @@ - - */ -class AuthServiceLoginTest extends IntegrationTest -{ - /** - * @var AuthService - */ - protected $authService; - - protected function setUp(): void - { - parent::setUp(); - $this->authService = resolve(AuthService::class); - } - - public function test_password_broker_facade() - { - $this->assertInstanceOf(PasswordBroker::class, $this->authService->broker()); - } - - public function test_login_throw_invalid_credentials_exception() - { - $this->expectException(CredentialsDoesntMatch::class); - Auth::shouldReceive('attempt') - ->andReturnFalse(); - $this->authService->login([ - 'email' => 'random@random.com', - 'password' => 'secret', - ]); - } - - public function test_user_did_not_verified_email() - { - $this->expectException(UnverifiedUser::class); - $this->expectExceptionMessage('The email is not verified'); - - $userMustVerify = (new class extends User implements MustVerifyEmail { - use \Illuminate\Auth\MustVerifyEmail; - }); - - $userMustVerify->fill([ - 'email' => 'test@mail.com', - 'email_verified_at' => null, - ]); - - Auth::shouldReceive('attempt') - ->andReturnTrue(); - - Auth::shouldReceive('user') - ->andReturn($userMustVerify); - - $this->authService->login([]); - } - - public function test_login_user_did_not_user_passport_trait_or_not_implement_pasportable() - { - $this->app['config']->set('restify.auth.provider', 'passport'); - $this->expectException(PassportUserException::class); - $userMustVerify = (new class extends User implements MustVerifyEmail { - use \Illuminate\Auth\MustVerifyEmail; - }); - - $userMustVerify->fill([ - 'email' => 'test@mail.com', - 'email_verified_at' => Carbon::now(), - ]); - - Auth::shouldReceive('attempt') - ->andReturnTrue(); - - Auth::shouldReceive('user') - ->andReturn($userMustVerify); - - $this->authService->login(); - } - - public function test_login_user_did_not_user_passport_trait_or_not_implement_airlockable() - { - $this->app['config']->set('restify.auth.provider', 'airlock'); - $this->expectException(AirlockUserException::class); - $userMustVerify = (new class extends User implements MustVerifyEmail { - use \Illuminate\Auth\MustVerifyEmail; - }); - - $userMustVerify->fill([ - 'email' => 'test@mail.com', - 'email_verified_at' => Carbon::now(), - ]); - - Auth::shouldReceive('attempt') - ->andReturnTrue(); - - Auth::shouldReceive('user') - ->andReturn($userMustVerify); - - $this->authService->login(); - } - - public function test_login_with_success() - { - Event::fake([ - UserLoggedIn::class, - ]); - $user = (new class extends User implements MustVerifyEmail, Passportable { - use \Illuminate\Auth\MustVerifyEmail; - }); - - $user->fill([ - 'email' => 'test@mail.com', - 'email_verified_at' => Carbon::now(), - ]); - - Auth::shouldReceive('attempt') - ->andReturnTrue(); - - Auth::shouldReceive('user') - ->andReturn($user); - - $authToken = $this->authService->login(); - $this->assertEquals('token', $authToken); - - Event::assertDispatched(UserLoggedIn::class, function ($e) use ($user) { - $this->assertEquals($e->user->email, $user->email); - - return $e->user instanceof User; - }); - } - - public function test_logout_success() - { - Event::fake(); - $this->app['config']->set('restify.auth.provider', 'passport'); - - $user = (new class extends \ Binaryk\LaravelRestify\Tests\Fixtures\User\User { - public function tokens() - { - $builder = \Mockery::mock(Builder::class); - $tokens = [(new class { - public function revoke() - { - return true; - } - })]; - - $builder->shouldReceive('get')->andReturn(collect($tokens)); - - return $builder; - } - }); - - Auth::shouldReceive('user') - ->andReturn($user); - - $this->authService->logout(); - - Event::assertDispatched(UserLogout::class); - } - - public function test_logout_unauthenticated() - { - Auth::shouldReceive('user') - ->andReturn(null); - - $this->expectException(AuthenticatableUserException::class); - $this->authService->logout(); - } -} diff --git a/tests/Feature/Authentication/AuthServiceRegisterTest.php b/tests/Feature/Authentication/AuthServiceRegisterTest.php index c2e292843..68ac30108 100644 --- a/tests/Feature/Authentication/AuthServiceRegisterTest.php +++ b/tests/Feature/Authentication/AuthServiceRegisterTest.php @@ -5,7 +5,6 @@ use Binaryk\LaravelRestify\Contracts\Passportable; use Binaryk\LaravelRestify\Exceptions\AuthenticatableUserException; use Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException; -use Binaryk\LaravelRestify\Http\Requests\RestifyRegisterRequest; use Binaryk\LaravelRestify\Models\LaravelRestifyModel; use Binaryk\LaravelRestify\Services\AuthService; use Binaryk\LaravelRestify\Tests\Fixtures\User\SimpleUser; @@ -15,9 +14,9 @@ use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Events\Verified; use Illuminate\Foundation\Testing\Concerns\InteractsWithContainer; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Event; use Illuminate\Support\Str; -use Illuminate\Validation\ValidationException; /** * @author Eduard Lupacescu @@ -49,8 +48,12 @@ public function test_register_throw_user_not_authenticatable() 'remember_token' => Str::random(10), ]; + $request = new Request([], []); + + $request->merge($user); + $this->expectException(AuthenticatableUserException::class); - $this->authService->register($user); + $this->authService->register($request); } public function test_user_query_throw_container_does_not_have_model_reflection_exception() @@ -83,7 +86,11 @@ public function test_register_successfully() 'remember_token' => Str::random(10), ]; - $this->authService->register($user); + $request = new Request([], []); + + $request->merge($user); + + $this->authService->register($request); Event::assertDispatched(Registered::class, function ($e) use ($user) { $this->assertEquals($e->user->email, $user['email']); @@ -112,7 +119,11 @@ public function test_verify_user_throw_hash_not_match() 'remember_token' => Str::random(10), ]; - $this->authService->register($user); + $request = new Request([], []); + + $request->merge($user); + + $this->authService->register($request); $lastUser = User::query()->get()->last(); $this->expectException(AuthorizationException::class); @@ -136,7 +147,11 @@ public function test_verify_user_successfully() 'remember_token' => Str::random(10), ]; - $this->authService->register($user); + $request = new Request([], []); + + $request->merge($user); + + $this->authService->register($request); $lastUser = User::query()->get()->last(); $this->assertNull($lastUser->email_verified_at); @@ -149,46 +164,4 @@ public function test_verify_user_successfully() return $e->user instanceof \ Binaryk\LaravelRestify\Tests\Fixtures\User\User; }); } - - public function test_register_invalid_payload_is_validated_on_register() - { - $user = [ - 'name' => 'Eduard Lupacescu', - 'email' => 'eduard.lupacescu@binarcode.com', - 'password' => 'password', - 'remember_token' => Str::random(10), - ]; - - AuthService::$registerFormRequest = RestifyRegisterRequest::class; - $this->expectException(ValidationException::class); - $this->authService->validateRegister($user); - AuthService::$registerFormRequest = null; - } - - public function test_register_payload_is_validated_on_register() - { - $user = [ - 'name' => 'Eduard Lupacescu', - 'email' => 'eduard.lupacescu@binarcode.com', - 'password' => 'password', - 'password_confirmation' => 'password', - 'remember_token' => Str::random(10), - ]; - - $this->assertTrue($this->authService->validateRegister($user)); - } - - public function test_invalid_payload_not_validated_because_validation_disabled() - { - AuthService::$registerFormRequest = null; - - $user = [ - 'name' => 'Eduard Lupacescu', - 'email' => 'eduard.lupacescu@binarcode.com', - 'password' => 'password', - 'remember_token' => Str::random(10), - ]; - - $this->assertTrue($this->authService->validateRegister($user)); - } } diff --git a/tests/Fixtures/User/User.php b/tests/Fixtures/User/User.php index 828b01778..b072ec989 100644 --- a/tests/Fixtures/User/User.php +++ b/tests/Fixtures/User/User.php @@ -2,8 +2,8 @@ namespace Binaryk\LaravelRestify\Tests\Fixtures\User; -use Binaryk\LaravelRestify\Contracts\Passportable; use Binaryk\LaravelRestify\Contracts\RestifySearchable; +use Binaryk\LaravelRestify\Contracts\Sanctumable; use Binaryk\LaravelRestify\Tests\Fixtures\Post\Post; use Binaryk\LaravelRestify\Traits\InteractWithSearch; use Illuminate\Contracts\Auth\MustVerifyEmail; @@ -15,7 +15,7 @@ /** * @author Eduard Lupacescu */ -class User extends Authenticatable implements Passportable, MustVerifyEmail, RestifySearchable +class User extends Authenticatable implements Sanctumable, MustVerifyEmail, RestifySearchable { use \Illuminate\Auth\MustVerifyEmail; use Notifiable,