├── .htaccess ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── config └── config.php ├── database ├── factories │ ├── UserFactory.php │ └── VerifiableUserFactory.php └── migrations │ ├── 2020_01_18_000001_create_mobile_verification_tokens_table.php │ └── 2022_01_19_000001_add_sent_at_in_tokens_table.php ├── lang └── en │ └── mobile_verifier.php └── src ├── Concerns ├── MustVerifyMobile.php ├── RedirectsUsers.php ├── Responses.php └── VerifiesMobiles.php ├── Contracts ├── MustVerifyMobile.php └── SMSClient.php ├── EventServiceProvider.php ├── Events └── Verified.php ├── Exceptions ├── InvalidTokenException.php └── SMSClientNotFoundException.php ├── Http ├── Controllers │ ├── BaseVerificationController.php │ ├── BaseVerificationControllerInterface.php │ └── MobileVerificationController.php ├── Middleware │ └── EnsureMobileIsVerified.php ├── Requests │ └── VerificationRequest.php └── routes.php ├── Listeners ├── AbstractMobileVerificationListener.php ├── SendMobileVerificationNotification.php └── SendMobileVerificationNotificationQueueable.php ├── Notifications ├── Channels │ └── VerificationChannel.php ├── Messages │ ├── MobileVerificationMessage.php │ └── Payload.php └── VerifyMobile.php ├── ServiceProvider.php └── Tokens ├── AbstractTokenRepository.php ├── CacheTokenRepository.php ├── DatabaseTokenRepository.php ├── TokenBroker.php ├── TokenBrokerInterface.php ├── TokenRepositoryInterface.php └── TokenRepositoryManager.php /.htaccess: -------------------------------------------------------------------------------- 1 | Deny from all -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 4.4.0 - 2025-04-16 2 | - Add support Laravel 12 3 | 4 | ## 4.2.0 - 2024-04-11 5 | - Add support for Laravel 11.x 6 | - Laravel 9 is deprecated 7 | 8 | ## 4.0.0 - 2023-03-29 9 | - Add support for Laravel V10 10 | 11 | ## 3.0.1 - 2022-11-11 12 | - Fix some bugs and add some env in config file 13 | - Fixed migration updating a table instead of trying to create it when already exists (#177) 14 | 15 | ## 3.0.0 - 2022-02-15 16 | - Add Support PHP 8.0 and Laravel 9.0 17 | - Deprecated PHP <= 7.* and Laravel <=8.* 18 | 19 | ## 2.7.0 - 2022-01-20 20 | - Add **getLatestSentAt** method to the token broker(#171) 21 | - Fix some bugs. 22 | 23 | ## 2.6.1 - 2021-04-01 24 | - Hotfix solved and add some languages for messaging(#165) 25 | 26 | ## 2.6.0 - 2021-03-29 27 | - Change the default storage driver to cache from database. 28 | - Refactor and cleanup (#164) 29 | 30 | ## 2.5.0 - 2021-03-25 31 | - Add support cache storage for saving tokens (#163) 32 | - Append mobile field to fillable attributes automatically for model. 33 | - Fix lang messages response 34 | 35 | ## 2.1.0 - 2021-03-10 36 | - Add support PHP 8 37 | 38 | ## 2.0.0 - 2020-11-14 39 | - Add Laravel 8 Support 40 | - Deprecated Laravel 5 41 | 42 | ## 1.2.0 - 2020-04-23 43 | - Add RedirectsUsers trait to package (Due to its removal in L7) 44 | - Add middleware explicitly 45 | - Remove support of php 7.1 46 | 47 | ## 1.1.0 - 2020-03-03 48 | - Add support Laravel 7 49 | 50 | ## 1.0.0 - 2020-01-26 51 | - First Stable Version 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 M.Fouladgar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Mobile Verification 2 | 3 | ![alt text](./cover.jpg "EloquentBuilder") 4 | 5 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/fouladgar/laravel-mobile-verification.svg)](https://packagist.org/packages/fouladgar/laravel-mobile-verification) 6 | ![Test Status](https://img.shields.io/github/actions/workflow/status/mohammad-fouladgar/laravel-mobile-verification/run-tests.yml?label=tests) 7 | ![Code Style Status](https://img.shields.io/github/actions/workflow/status/mohammad-fouladgar/laravel-mobile-verification/php-cs-fixer.yml?label=code%20style) 8 | ![Total Downloads](https://img.shields.io/packagist/dt/fouladgar/laravel-mobile-verification) 9 | 10 | ## Introduction 11 | 12 | Many web applications require users to verify their mobile phone numbers before using the application. Rather than 13 | forcing you to re-implement this on each application, this package provides convenient methods for sending and verifying 14 | mobile phone verification requests. 15 | 16 | ## Version Compatibility 17 | 18 | | Laravel | LaravelMobileVerification | 19 | |:--------------|:--------------------------| 20 | | 12.0.x | 4.4.x | 21 | | 11.0.x | 4.2.x | 22 | | 10.0.x | 4.0.x | 23 | | 9.0.x | 3.0.x | 24 | | 6.0.x to 8.0x | 2.0.x | 25 | | 5.0.x | 1.2.0 | 26 | 27 | ## Installation 28 | 29 | You can install the package via composer: 30 | 31 | ```shell 32 | composer require fouladgar/laravel-mobile-verification 33 | ``` 34 | 35 | ## Configuration 36 | 37 | To get started, you should publish the `config/mobile_verifier.php` config file with: 38 | 39 | ``` 40 | php artisan vendor:publish --provider="Fouladgar\MobileVerification\ServiceProvider" --tag="config" 41 | ``` 42 | 43 | ### Token Storage 44 | 45 | After generating a token, we need to store it in a storage. This package supports two drivers: `cache` and `database` 46 | which the default driver is `cache`. You may specify which storage driver you would like to be used for saving tokens in 47 | your application: 48 | 49 | ```php 50 | // config/mobile_verifier.php 51 | 52 | 'cache', 59 | ]; 60 | ``` 61 | 62 | ##### Database 63 | 64 | It means after migrating, a table will be created which your application needs to store verification tokens. 65 | 66 | > If you’re using another table name for `users` table or different column name for `mobile` phone or 67 | > even `mobile_verification_tokens` table, you can customize their values in config file: 68 | 69 | ```php 70 | // config/mobile_verifier.php 71 | 72 | 'users', 77 | 78 | 'mobile_column' => 'mobile', 79 | 80 | 'token_table' => 'mobile_verification_tokens', 81 | 82 | //... 83 | ]; 84 | ``` 85 | 86 | ##### Cache 87 | 88 | When using the `cache` driver, the token will be stored in a cache driver configured by your application. In this case, 89 | your application performance is more than when using database definitely. 90 | 91 | All right! Now you should migrate the database: 92 | 93 | ``` 94 | php artisan migrate 95 | ``` 96 | 97 | Depending on the `token_storage` config, the package migration will create a token table. Also, a `mobile_verified_at` 98 | and `mobile` column will be added to your `users` table to show user verification state and store user's mobile phone. 99 | 100 | ### Model Preparation 101 | 102 | In the following, make sure your `User` model implements the `Fouladgar\MobileVerification\Contracts\MustVerifyMobile` 103 | contract and use the `Fouladgar\MobileVerification\Concerns\MustVerifyMobile` trait: 104 | 105 | ```php 106 | SMSService 154 | ->send($payload->getTo(), $payload->getToken()); 155 | } 156 | 157 | // ... 158 | } 159 | ``` 160 | 161 | > In above example, `SMSService` can be replaced with your chosen SMS service along with its respective method. 162 | 163 | Next, you should set the your `SMSClient` class in config file: 164 | 165 | ```php 166 | // config/mobile_verifier.php 167 | 168 | App\SampleSMSClient::class, 173 | 174 | //... 175 | ]; 176 | ``` 177 | 178 | ## Usage 179 | 180 | Now you are ready for sending a verification message! You just need to dispatch the `Illuminate\Auth\Events\Registered` 181 | event after registering user: 182 | 183 | ```php 184 | Notice: You should choose a long with middleware which you are going to use them for above APIs through set it in 227 | > config file: 228 | 229 | ```php 230 | // config/mobile_verifier.php 231 | 232 | ['auth:sanctum'], 237 | 238 | //... 239 | ]; 240 | ``` 241 | 242 | ### Customize Routes and Controller 243 | 244 | In order to change default routes prefix or routes themselves, you can customize them in config file: 245 | 246 | ```php 247 | // config/mobile_verifier.php 248 | 249 | 'auth', 254 | 255 | 'routes' => [ 256 | 'verify' => '/mobile/verify', 257 | 'resend' => '/mobile/resend', 258 | ], 259 | 260 | //... 261 | ]; 262 | ``` 263 | 264 | Also, this package allows you to override default controller. To achieve this, you can extend your controller 265 | from `Fouladgar\MobileVerification\Http\Controllers\BaseVerificationController` and set your controller namespace in 266 | config file: 267 | 268 | ```php 269 | // config/mobile_verifier.php 270 | 271 | 'App\Http\Controllers', 276 | 277 | //... 278 | ]; 279 | ``` 280 | 281 | > **Note:** You can only change controller namespace and name of the controller should remain as package default 282 | > controller (`MobileVerificationController`) 283 | 284 | ```php 285 | **Important note**: If you set header request to `Accept:application/json`, the response will be in Json format, 304 | > otherwise the user will automatically be redirected to `/home`. You can customize the post verification redirect 305 | > location by defining a `redirectTo` method or property on the `MobileVerificationController`. 306 | 307 | ## Protecting Routes 308 | 309 | Route middleware can be used to only allow verified users to access a given route. This package ships with a verified 310 | middleware, which is defined at `Fouladgar\MobileVerification\Http\Middleware`. Since this middleware is already 311 | registered in your application's HTTP kernel, all you need to do is attach the middleware to a route definition: 312 | 313 | ```php 314 | Route::get('profile', function () { 315 | // Only verified users may enter... 316 | })->middleware('mobile.verified'); 317 | ``` 318 | 319 | ## Using Queue 320 | 321 | By default, this package does not process sending verification messages in the queue. But if you want your sending 322 | messages to be queued, you may change `connection` value from `sync` to your preferred queue connection. 323 | And be sure to config your queue connection in your `.env` file. 324 | You are allowed to to change other queue settings here in the config. 325 | 326 | ```php 327 | // config/mobile_verifier.php 328 | [ 335 | 'connection' => 'sync', 336 | 'queue' => 'default', 337 | 'tries' => 3, 338 | 'timeout' => 60, 339 | ] 340 | ]; 341 | ``` 342 | 343 | ## Translates and Views 344 | 345 | To publish translation file you may use this command: 346 | 347 | ``` 348 | php artisan vendor:publish --provider="Fouladgar\MobileVerification\ServiceProvider" --tag="lang" 349 | ``` 350 | 351 | If you are not using AJAX requests, you should have some views which we provided you some information through session 352 | variables. In case of errors, you just need to use laravel default `$errors` variable. In case of successful 353 | verification, you can use `mobileVerificationVerified` variable and for successful resend verification you may 354 | use `mobileVerificationResend` variable. These variables contain messages which you can customize in provided language 355 | file: 356 | 357 | ```php 358 | // lang/vendor/MobileVerification/en/mobile_verification.php 359 | 360 | 'Your mobile has been verified successfully.', 364 | 'successful_resend' => 'The token has been resent successfully.', 365 | 'already_verified' => 'Your mobile already has been verified.', 366 | //etc... 367 | ]; 368 | ``` 369 | 370 | ## Event 371 | 372 | This package dispatch an event during the mobile verification process. You may attach listeners to this event in 373 | your `EventServiceProvider`: 374 | 375 | ```php 376 | /** 377 | * The event listener mappings for the application. 378 | * 379 | * @var array 380 | */ 381 | protected $listen = [ 382 | 'Fouladgar\MobileVerification\Events\Verified' => [ 383 | 'App\Listeners\LogVerifiedUser', 384 | ], 385 | ]; 386 | ``` 387 | 388 | ## Testing 389 | 390 | ```sh 391 | composer test 392 | ``` 393 | 394 | ## Changelog 395 | 396 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 397 | 398 | ## Contributing 399 | 400 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 401 | 402 | ## Security 403 | 404 | If you discover any security related issues, please email fouladgar.dev@gmail.com instead of using the issue tracker. 405 | 406 | ## License 407 | 408 | Laravel-Mobile-Verification is released under the MIT License. See the bundled 409 | [LICENSE](https://github.com/mohammad-fouladgar/laravel-mobile-verification/blob/master/LICENSE) 410 | file for details. 411 | 412 | Built with :heart: for you. 413 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fouladgar/laravel-mobile-verification", 3 | "description": "This package provides convenient methods for making token code, sending and verifying mobile phone verification requests.", 4 | "keywords": [ 5 | "mobile-verification", 6 | "mobile-verifier", 7 | "laravel-mobile-verification", 8 | "laravel", 9 | "lumen", 10 | "mobile" 11 | ], 12 | "support": { 13 | "issues": "https://github.com/mohammad-fouladgar/laravel-mobile-verification/issues", 14 | "source": "https://github.com/mohammad-fouladgar/laravel-mobile-verification" 15 | }, 16 | "authors": [ 17 | { 18 | "name": "Mohammad Fouladgar", 19 | "email": "fouladgar.dev@gmail.com", 20 | "role": "Developer" 21 | } 22 | ], 23 | "license": "MIT", 24 | "require": { 25 | "php": "^8.2", 26 | "illuminate/database": "^10.0|^11.0|^12.0", 27 | "illuminate/support": "^10.0|^11.0|^12.0", 28 | "illuminate/notifications": "^10.0|^11.0|^12.0" 29 | }, 30 | "require-dev": { 31 | "phpunit/phpunit": "^10.5.17|^11.5.3", 32 | "orchestra/testbench": "^7.0|^8.0|^10.0", 33 | "mockery/mockery": "^1.4", 34 | "php-coveralls/php-coveralls": "^2.1" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Fouladgar\\MobileVerification\\": "src/", 39 | "Fouladgar\\MobileVerification\\Database\\Factories\\": "database/factories" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "Fouladgar\\MobileVerification\\Tests\\": "tests/" 45 | } 46 | }, 47 | "scripts": { 48 | "test": "vendor/bin/phpunit" 49 | }, 50 | "extra": { 51 | "laravel": { 52 | "providers": [ 53 | "Fouladgar\\MobileVerification\\ServiceProvider" 54 | ] 55 | } 56 | }, 57 | "config": { 58 | "discard-changes": true 59 | }, 60 | "minimum-stability": "dev", 61 | "prefer-stable": true 62 | } 63 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | 'users', 14 | 15 | /* 16 | |-------------------------------------------------------------------------- 17 | | Default Mobile Column 18 | |-------------------------------------------------------------------------- 19 | | 20 | | Here you should specify name of your column (in users table) which user 21 | | mobile number reside in. 22 | | 23 | */ 24 | 'mobile_column' => 'mobile', 25 | 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Default Verification Tokens Table Name 29 | |-------------------------------------------------------------------------- 30 | | 31 | | Here you should specify name of your verification tokens table in database. 32 | | This table will held all information about created verification tokens for users. 33 | | 34 | */ 35 | 'token_table' => 'mobile_verification_tokens', 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | Verification Token Length 40 | |-------------------------------------------------------------------------- 41 | | 42 | | Here you can specify length of verification tokens which will send to users. 43 | | 44 | */ 45 | 'token_length' => env('MOBILE_VERIFICATION_TOKEN_LENGTH', 5), 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Verification Token Lifetime 50 | |-------------------------------------------------------------------------- 51 | | 52 | | Here you can specify lifetime of verification tokens (in minutes) which will send to users. 53 | | 54 | */ 55 | 'token_lifetime' => env('MOBILE_VERIFICATION_TOKEN_LIFE_TIME', 5), 56 | 57 | /* 58 | |-------------------------------------------------------------------------- 59 | | SMS Client (REQUIRED) 60 | |-------------------------------------------------------------------------- 61 | | 62 | | Here you should specify your implemented "SMS Client" class. This class is 63 | | responsible for sending SMS to users. 64 | | 65 | */ 66 | 'sms_client' => '', 67 | 68 | /* 69 | |-------------------------------------------------------------------------- 70 | | Token Storage Driver 71 | |-------------------------------------------------------------------------- 72 | | 73 | | Here you may define token "storage" driver. If you choose the "cache", the token will be stored 74 | | in a cache driver configured by your application. Otherwise, a table will be created for storing tokens. 75 | | 76 | | Supported drivers: "cache", "database" 77 | | 78 | */ 79 | 'token_storage' => env('MOBILE_VERIFICATION_TOKEN_STORAGE', 'cache'), 80 | 81 | /* 82 | |-------------------------------------------------------------------------- 83 | | Default Controller Namespace 84 | |-------------------------------------------------------------------------- 85 | | 86 | | This is the namespace of default controller. Feel free 87 | | to change this namespace to anything you like. 88 | */ 89 | 'controller_namespace' => 'Fouladgar\MobileVerification\Http\Controllers', 90 | 91 | /* 92 | |-------------------------------------------------------------------------- 93 | | Routes Prefix 94 | |-------------------------------------------------------------------------- 95 | | 96 | | This is the routes prefix where Mobile-Verifier controller will be accessible from. Feel free 97 | | to change this path to anything you like. 98 | | 99 | */ 100 | 'routes_prefix' => 'auth', 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | Controller Routes 105 | |-------------------------------------------------------------------------- 106 | | 107 | | Here you can specify your desired routes for verify and resend actions. 108 | | 109 | */ 110 | 'routes' => [ 111 | 'verify' => '/mobile/verify', 112 | 'resend' => '/mobile/resend', 113 | ], 114 | 115 | /* 116 | |-------------------------------------------------------------------------- 117 | | Throttle 118 | |-------------------------------------------------------------------------- 119 | | 120 | | Here you can specify throttle for verify/resend routes 121 | | 122 | */ 123 | 'throttle' => 10, 124 | 125 | /* 126 | |-------------------------------------------------------------------------- 127 | | Middleware 128 | |-------------------------------------------------------------------------- 129 | | 130 | | Here you can specify which middleware you want to use for the APIs 131 | | For example: 'web', 'auth', 'auth:api', 'auth:sanctum' 132 | | 133 | */ 134 | 'middleware' => ['auth'], 135 | 136 | /* 137 | |-------------------------------------------------------------------------- 138 | | Queue 139 | |-------------------------------------------------------------------------- 140 | | 141 | | By default, This package does not queue sending verification messages. 142 | | But if you want your messages to process in a queue, change connection from sync to your preferred connection. 143 | | Be sure to config your queue settings in your .env file if you want to enable queue. 144 | | 145 | | Supported drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 146 | | 147 | */ 148 | 'queue' => [ 149 | 'connection' => 'sync', 150 | 'queue' => 'default', 151 | 'tries' => 3, 152 | 'timeout' => 60, 153 | ] 154 | ]; 155 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->firstName(), 22 | 'mobile' => $this->faker->phoneNumber(), 23 | ]; 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /database/factories/VerifiableUserFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->name, 23 | 'mobile' => '555555', 24 | 'mobile_verified_at' => null, 25 | ]; 26 | } 27 | 28 | public function verified(): static 29 | { 30 | return $this->state(fn(array $attributes) => [ 31 | 'mobile_verified_at' => Carbon::now(), 32 | ]); 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /database/migrations/2020_01_18_000001_create_mobile_verification_tokens_table.php: -------------------------------------------------------------------------------- 1 | userTable = config('mobile_verifier.user_table', 'users'); 18 | $this->mobileColumn = config('mobile_verifier.mobile_column', 'mobile'); 19 | $this->tokenTable = config('mobile_verifier.token_table', 'mobile_verification_tokens'); 20 | } 21 | 22 | public function up(): void 23 | { 24 | $this->tokenTableUp(); 25 | 26 | if (!Schema::hasColumn($this->userTable, $this->mobileColumn)) { 27 | Schema::table( 28 | $this->userTable, 29 | function (Blueprint $table): void { 30 | $table->string($this->mobileColumn); 31 | } 32 | ); 33 | } 34 | 35 | if (!Schema::hasColumn($this->userTable, 'mobile_verified_at')) { 36 | Schema::table( 37 | $this->userTable, 38 | function (Blueprint $table): void { 39 | $table->timestamp('mobile_verified_at')->nullable()->after($this->mobileColumn); 40 | } 41 | ); 42 | } 43 | } 44 | 45 | public function down(): void 46 | { 47 | $this->tokenTableDown(); 48 | 49 | Schema::table( 50 | $this->userTable, 51 | static function (Blueprint $table): void { 52 | $table->dropColumn('mobile_verified_at'); 53 | } 54 | ); 55 | } 56 | 57 | private function tokenTableUp(): void 58 | { 59 | if (config('mobile_verifier.token_storage') === 'cache') { 60 | return; 61 | } 62 | 63 | Schema::create( 64 | $this->tokenTable, 65 | static function (Blueprint $table): void { 66 | $table->increments('id'); 67 | $table->string('mobile')->index(); 68 | $table->string('token', 10)->index(); 69 | $table->timestamp('expires_at')->nullable(); 70 | 71 | $table->index(['mobile', 'token']); 72 | } 73 | ); 74 | } 75 | 76 | private function tokenTableDown(): void 77 | { 78 | if (config('mobile_verifier.token_storage') === 'cache') { 79 | return; 80 | } 81 | 82 | Schema::drop($this->tokenTable); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /database/migrations/2022_01_19_000001_add_sent_at_in_tokens_table.php: -------------------------------------------------------------------------------- 1 | tokenTable = config('mobile_verifier.token_table', 'mobile_verification_tokens'); 14 | } 15 | 16 | public function up(): void 17 | { 18 | if (config('mobile_verifier.token_storage') === 'cache') { 19 | return; 20 | } 21 | 22 | Schema::table($this->tokenTable, function (Blueprint $table) { 23 | $table->timestamp('sent_at')->nullable(); 24 | }); 25 | } 26 | 27 | public function down(): void 28 | { 29 | if (config('mobile_verifier.token_storage') === 'cache') { 30 | return; 31 | } 32 | 33 | Schema::table($this->tokenTable, function (Blueprint $table) { 34 | $table->dropColumn('sent_at'); 35 | }); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /lang/en/mobile_verifier.php: -------------------------------------------------------------------------------- 1 | 'Your mobile has been verified successfully.', 15 | 'successful_resend' => 'The token has been resent successfully.', 16 | 'already_verified' => 'Your mobile already has been verified.', 17 | 'expired_or_invalid' => 'The token has been expired or invalid.', 18 | 'not_verified' => 'Your mobile number is not verified.', 19 | ]; 20 | -------------------------------------------------------------------------------- /src/Concerns/MustVerifyMobile.php: -------------------------------------------------------------------------------- 1 | appendMobileFieldToFillableAttributes(); 14 | 15 | return $this->fillable; 16 | } 17 | 18 | /** 19 | * Append mobile filed to fillable attributes for model. 20 | */ 21 | private function appendMobileFieldToFillableAttributes(): void 22 | { 23 | $mobileFiled = $this->getMobileField(); 24 | 25 | if (! in_array($mobileFiled, $this->fillable, true)) { 26 | $this->fillable = array_merge($this->fillable, [$mobileFiled]); 27 | } 28 | } 29 | 30 | /** 31 | * Get mobile phone field name. 32 | */ 33 | private function getMobileField(): string 34 | { 35 | return config('mobile_verifier.mobile_column', 'mobile'); 36 | } 37 | 38 | /** 39 | * Determine if the user has verified their mobile number. 40 | */ 41 | public function hasVerifiedMobile(): bool 42 | { 43 | return $this->mobile_verified_at !== null; 44 | } 45 | 46 | /** 47 | * Mark the given user's mobile as verified. 48 | */ 49 | public function markMobileAsVerified(): bool 50 | { 51 | return $this->forceFill(['mobile_verified_at' => $this->freshTimestamp()])->save(); 52 | } 53 | 54 | /** 55 | * Send the mobile verification notification. 56 | */ 57 | public function sendMobileVerifierNotification(string $token): void 58 | { 59 | $this->notify(new VerifyMobile($token)); 60 | } 61 | 62 | /** 63 | * Get the mobile number that should be used for verification. 64 | */ 65 | public function getMobileForVerification(): string 66 | { 67 | return $this->{$this->getMobileField()}; 68 | } 69 | 70 | /** 71 | * Get the recipients of the given message. 72 | */ 73 | public function routeNotificationForVerificationMobile(): string 74 | { 75 | return $this->{$this->getMobileField()}; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Concerns/RedirectsUsers.php: -------------------------------------------------------------------------------- 1 | redirectTo(); 16 | } 17 | 18 | return property_exists($this, 'redirectTo') ? $this->redirectTo : '/home'; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Concerns/Responses.php: -------------------------------------------------------------------------------- 1 | json(['message' => __('MobileVerification::mobile_verifier.successful_verification')]); 14 | } 15 | 16 | protected function successResendMessage(): JsonResponse 17 | { 18 | return response()->json(['message' => __('MobileVerification::mobile_verifier.successful_resend')]); 19 | } 20 | 21 | protected function unprocessableEntity(): JsonResponse 22 | { 23 | return response()->json(['message' => __('MobileVerification::mobile_verifier.already_verified')], 422); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Concerns/VerifiesMobiles.php: -------------------------------------------------------------------------------- 1 | user(); 24 | 25 | if ($user->hasVerifiedMobile()) { 26 | return $request->expectsJson() ? $this->unprocessableEntity() : redirect($this->redirectPath()); 27 | } 28 | 29 | try { 30 | $this->tokenBroker->verifyToken($user, $request->token); 31 | } catch (InvalidTokenException $e) { 32 | return $request->expectsJson() 33 | ? response()->json(['message' => $e->getMessage()], $e->getCode()) 34 | : back()->withErrors(['token' => $e->getMessage()]); 35 | } 36 | 37 | event(new Verified($user, $request->all())); 38 | 39 | return $request->expectsJson() 40 | ? $this->successMessage() 41 | : redirect($this->redirectPath())->with( 42 | 'mobileVerificationVerified', 43 | __('MobileVerification::mobile_verifier.successful_verification') 44 | ); 45 | } 46 | 47 | public function resend(Request $request): ViewFactory|JsonResponse|Redirector|RedirectResponse 48 | { 49 | $user = $request->user(); 50 | 51 | if ($user->hasVerifiedMobile()) { 52 | return $request->expectsJson() ? $this->unprocessableEntity() : redirect($this->redirectPath()); 53 | } 54 | 55 | $this->tokenBroker->sendToken($user); 56 | 57 | return $request->expectsJson() 58 | ? $this->successResendMessage() 59 | : back()->with('mobileVerificationResend', __('MobileVerification::mobile_verifier.successful_resend')); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Contracts/MustVerifyMobile.php: -------------------------------------------------------------------------------- 1 | middleware(config('mobile_verifier.middleware', ['web', 'auth'])); 18 | 19 | $throttle = config('mobile_verifier.throttle', 10); 20 | 21 | $this->middleware("throttle:$throttle,1")->only('verify', 'resend'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Http/Controllers/BaseVerificationControllerInterface.php: -------------------------------------------------------------------------------- 1 | user(); 15 | 16 | if (! $user || ($user instanceof MustVerifyMobile && ! $user->hasVerifiedMobile())) { 17 | return $request->expectsJson() 18 | ? abort(403, __('MobileVerification::mobile_verifier.not_verified')) 19 | : redirect('/')->withErrors(['mobile' => __('MobileVerification::mobile_verifier.not_verified')]); 20 | } 21 | 22 | return $next($request); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Http/Requests/VerificationRequest.php: -------------------------------------------------------------------------------- 1 | 'required|string|size:' . config('mobile_verifier.token_length', 5), 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Http/routes.php: -------------------------------------------------------------------------------- 1 | name('mobile.verify'); 9 | Route::post($resendRoute, 'MobileVerificationController@resend')->name('mobile.resend'); 10 | -------------------------------------------------------------------------------- /src/Listeners/AbstractMobileVerificationListener.php: -------------------------------------------------------------------------------- 1 | user; 25 | 26 | if ($user instanceof MustVerifyMobile && ! $user->hasVerifiedMobile()) { 27 | $this->tokenBroker->sendToken($user); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Listeners/SendMobileVerificationNotification.php: -------------------------------------------------------------------------------- 1 | config('mobile_verifier.queue.tries'), 15 | 'timeout' => config('mobile_verifier.queue.timeout'), 16 | 'connection' => config('mobile_verifier.queue.connection'), 17 | 'queue' => config('mobile_verifier.queue.queue'), 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Notifications/Channels/VerificationChannel.php: -------------------------------------------------------------------------------- 1 | routeNotificationFor('verification_mobile', $notification)) { 20 | return null; 21 | } 22 | 23 | /** @var MobileVerificationMessage $message */ 24 | $message = $notification->toMobile($notifiable); 25 | 26 | return $this->SMSClient->sendMessage($message->getPayload()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Notifications/Messages/MobileVerificationMessage.php: -------------------------------------------------------------------------------- 1 | to = $to; 16 | 17 | return $this; 18 | } 19 | 20 | public function token(string $token): self 21 | { 22 | $this->token = $token; 23 | 24 | return $this; 25 | } 26 | 27 | public function getPayload(): Payload 28 | { 29 | return (new Payload())->setTo($this->to)->setToken($this->token); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Notifications/Messages/Payload.php: -------------------------------------------------------------------------------- 1 | token; 16 | } 17 | 18 | public function setToken(string $token): self 19 | { 20 | $this->token = $token; 21 | 22 | return $this; 23 | } 24 | 25 | public function getTo(): string 26 | { 27 | return $this->to; 28 | } 29 | 30 | public function setTo(string $to): self 31 | { 32 | $this->to = $to; 33 | 34 | return $this; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Notifications/VerifyMobile.php: -------------------------------------------------------------------------------- 1 | to($notifiable->getMobileForVerification())->token($this->token); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerRoutes(); 25 | 26 | $this->loadAssetsFrom(); 27 | 28 | $this->registerPublishing(); 29 | 30 | $router->aliasMiddleware('mobile.verified', EnsureMobileIsVerified::class); 31 | } 32 | 33 | /** 34 | * Register any package services. 35 | */ 36 | public function register(): void 37 | { 38 | $this->app->register(EventServiceProvider::class); 39 | 40 | $this->mergeConfigFrom($this->getConfig(), 'mobile_verifier'); 41 | 42 | $this->registerBindings(); 43 | } 44 | 45 | /** 46 | * Register the package routes. 47 | */ 48 | protected function registerRoutes(): void 49 | { 50 | Route::group( 51 | $this->routeConfiguration(), 52 | function (): void { 53 | $this->loadRoutesFrom(__DIR__.'/Http/routes.php'); 54 | } 55 | ); 56 | } 57 | 58 | /** 59 | * Load and register package assets. 60 | */ 61 | protected function loadAssetsFrom(): void 62 | { 63 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 64 | 65 | $this->loadTranslationsFrom(__DIR__.'/../lang', 'MobileVerification'); 66 | } 67 | 68 | /** 69 | * Register the package's publishable resources. 70 | */ 71 | protected function registerPublishing(): void 72 | { 73 | $this->publishes([$this->getConfig() => config_path('mobile_verifier.php')], 'config'); 74 | 75 | $this->publishes([__DIR__.'/../lang' => app()->langPath().'/vendor/MobileVerification'], 'lang'); 76 | 77 | $this->publishes([__DIR__.'/../database/migrations' => database_path('migrations')], 'migrations'); 78 | } 79 | 80 | /** 81 | * Get the config file path. 82 | */ 83 | protected function getConfig(): string 84 | { 85 | return __DIR__.'/../config/config.php'; 86 | } 87 | 88 | /** 89 | * Register any package bindings. 90 | */ 91 | protected function registerBindings(): void 92 | { 93 | $this->app->singleton( 94 | SMSClient::class, 95 | static function ($app) { 96 | try { 97 | return $app->make(config('mobile_verifier.sms_client')); 98 | } catch (Throwable) { 99 | throw new SMSClientNotFoundException(); 100 | } 101 | } 102 | ); 103 | 104 | $this->app->singleton('mobile.verifier.token.repository', fn ($app) => new TokenRepositoryManager($app)); 105 | 106 | $this->app->singleton( 107 | TokenRepositoryInterface::class, 108 | fn ($app) => $app['mobile.verifier.token.repository']->driver() 109 | ); 110 | 111 | $this->app->bind(TokenBrokerInterface::class, TokenBroker::class); 112 | } 113 | 114 | /** 115 | * Get route group configuration array. 116 | */ 117 | private function routeConfiguration(): array 118 | { 119 | return [ 120 | 'namespace' => config( 121 | 'mobile_verifier.controller_namespace', 122 | 'Fouladgar\MobileVerification\Http\Controllers' 123 | ), 124 | 'prefix' => config('mobile_verifier.routes_prefix', 'auth'), 125 | ]; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Tokens/AbstractTokenRepository.php: -------------------------------------------------------------------------------- 1 | expires = $expires; 19 | 20 | return $this; 21 | } 22 | 23 | /** 24 | * @throws Exception 25 | */ 26 | protected function createNewToken(): string 27 | { 28 | $tokenLength = config('mobile_verifier.token_length', 5); 29 | 30 | return (string) random_int(10 ** ($tokenLength - 1), (10 ** $tokenLength) - 1); 31 | } 32 | 33 | protected function tokenExpired(string $expiresAt): bool 34 | { 35 | return Carbon::parse($expiresAt)->isPast(); 36 | } 37 | 38 | /** 39 | * Build the record payload for the table. 40 | */ 41 | protected function getPayload(string $mobile, string $token): array 42 | { 43 | return ['mobile' => $mobile, 'token' => $token, 'sent_at' => now()->toDateTimeString()]; 44 | } 45 | 46 | /** 47 | * Insert into token storage. 48 | */ 49 | abstract protected function insert(string $mobile, string $token): bool; 50 | } 51 | -------------------------------------------------------------------------------- /src/Tokens/CacheTokenRepository.php: -------------------------------------------------------------------------------- 1 | getMobileForVerification(); 15 | 16 | $this->deleteExisting($user); 17 | 18 | $token = $this->createNewToken(); 19 | 20 | $this->insert($mobile, $token); 21 | 22 | return $token; 23 | } 24 | 25 | public function deleteExisting(MustVerifyMobile $user): void 26 | { 27 | Cache::forget($user->getMobileForVerification()); 28 | } 29 | 30 | public function exists(MustVerifyMobile $user, string $token): bool 31 | { 32 | return Cache::has($user->getMobileForVerification()) && 33 | Cache::get($user->getMobileForVerification())['token'] === $token; 34 | } 35 | 36 | public function latestSentAt(MustVerifyMobile $user, string $token): string 37 | { 38 | $key = $user->getMobileForVerification(); 39 | if (! Cache::has($key)) { 40 | return ''; 41 | } 42 | 43 | return Cache::get($key)['sent_at']; 44 | } 45 | 46 | protected function insert(string $mobile, string $token): bool 47 | { 48 | return Cache::add($mobile, $this->getPayload($mobile, $token), now()->addMinutes($this->expires)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Tokens/DatabaseTokenRepository.php: -------------------------------------------------------------------------------- 1 | getMobileForVerification(); 29 | 30 | $this->deleteExisting($user); 31 | 32 | $token = $this->createNewToken(); 33 | 34 | $this->insert($mobile, $token); 35 | 36 | return $token; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function deleteExisting(MustVerifyMobile $user): void 43 | { 44 | optional($this->getTable()->where('mobile', $user->getMobileForVerification()))->delete(); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function exists(MustVerifyMobile $user, string $token): bool 51 | { 52 | $record = $this->getTokenRecord($user, $token); 53 | 54 | return $record && ! $this->tokenExpired($record['expires_at']); 55 | } 56 | 57 | public function latestSentAt(MustVerifyMobile $user, string $token): string 58 | { 59 | $tokenRow = $this->getTokenRecord($user, $token); 60 | 61 | if (! $tokenRow) { 62 | return ''; 63 | } 64 | 65 | return $tokenRow['sent_at']; 66 | } 67 | 68 | /** 69 | * Begin a new database query against the table. 70 | */ 71 | protected function getTable(): Builder 72 | { 73 | return $this->connection->table($this->table); 74 | } 75 | 76 | /** 77 | * @throws Exception 78 | */ 79 | protected function insert(string $mobile, string $token): bool 80 | { 81 | return $this->getTable()->insert($this->getPayload($mobile, $token)); 82 | } 83 | 84 | /** 85 | * @inheritDoc 86 | */ 87 | protected function getPayload(string $mobile, string $token): array 88 | { 89 | return parent::getPayload($mobile, $token) + ['expires_at' => now()->addMinutes($this->expires)]; 90 | } 91 | 92 | private function getTokenRecord(MustVerifyMobile $user, string $token): array 93 | { 94 | return (array) $this->getTable() 95 | ->where('mobile', $user->getMobileForVerification()) 96 | ->where('token', $token) 97 | ->first(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Tokens/TokenBroker.php: -------------------------------------------------------------------------------- 1 | sendMobileVerifierNotification( 22 | $this->tokenRepository->create($user) 23 | ); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function verifyToken(MustVerifyMobile $user, string $token): bool 30 | { 31 | throw_unless($this->tokenExists($user, $token), InvalidTokenException::class); 32 | 33 | $user->markMobileAsVerified(); 34 | 35 | $this->tokenRepository->deleteExisting($user); 36 | 37 | return true; 38 | } 39 | 40 | public function tokenExists(MustVerifyMobile $user, string $token): bool 41 | { 42 | return $this->tokenRepository->exists($user, $token); 43 | } 44 | 45 | public function getLatestSentAt(MustVerifyMobile $user, string $token): string 46 | { 47 | return $this->tokenRepository->latestSentAt($user, $token); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Tokens/TokenBrokerInterface.php: -------------------------------------------------------------------------------- 1 | config->get('mobile_verifier.token_storage', 'cache'); 13 | } 14 | 15 | protected function createCacheDriver(): TokenRepositoryInterface 16 | { 17 | return new CacheTokenRepository( 18 | $this->config->get('mobile_verifier.token_lifetime', 5), 19 | $this->config->get('mobile_verifier.token_length', 5) 20 | ); 21 | } 22 | 23 | protected function createDatabaseDriver(): TokenRepositoryInterface 24 | { 25 | return new DatabaseTokenRepository( 26 | $this->config->get('mobile_verifier.token_lifetime', 5), 27 | $this->config->get('mobile_verifier.token_length', 5), 28 | $this->config->get('mobile_verifier.token_table', 'mobile_verification_tokens'), 29 | $this->container->make(ConnectionInterface::class) 30 | ); 31 | } 32 | } 33 | --------------------------------------------------------------------------------