├── .DS_Store ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── requirepin.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── READMEOLD.md ├── Tests ├── CommandTest.php ├── Controllers │ └── BookController.php ├── Models │ └── Book.php ├── NotificationsTest.php ├── PinTest.php ├── Requests │ └── CreateBookRequest.php ├── ServiceProviderTest.php ├── TestCase.php └── migrations │ ├── 0001_01_01_000000_create_users_table.php │ └── 2022_09_12_005402_create_books_table.php ├── composer.json ├── coverage.xml ├── package-lock.json ├── package.json ├── phpunit.xml └── src ├── .DS_Store ├── Controllers └── PinController.php ├── Facades └── PinService.php ├── Middleware └── RequirePin.php ├── Models ├── OldPin.php ├── RequirePin.php ├── TestUser.php └── User.php ├── Notifications └── PinChange.php ├── Requests └── ChangePinRequest.php ├── RequirePinServiceProvider.php ├── Rules ├── CurrentPin.php └── DisallowOldPin.php ├── Services ├── PinService.php └── ThrottleRequestsService.php ├── Traits └── Helpers.php ├── config └── requirepin.php ├── lang └── en │ ├── general.php │ └── pin.php ├── migrations ├── .DS_Store ├── 2022_10_11_234205_add_column_pin_to_users_table.php ├── 2022_10_12_193200_create_require_pins_table.php └── 2022_10_12_224623_create_old_pins_table.php ├── routes ├── api.php └── web.php └── views ├── layouts └── app.blade.php └── pin ├── changepin.blade.php └── pinrequired.blade.php /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikechukwukalu/requirepin/66525a1b7311430f49f890b96a767be44bc8c393/.DS_Store -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: | 3 | File a bug report to be fixed. 4 | title: "[X.x] What does happen that is considered an error or bug?" 5 | labels: ["bug"] 6 | assignees: 7 | - ikechukwukalu 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: | 12 | Thanks for taking the time to fill out this bug report! 13 | 14 | The more detailed this bug report is, the faster it can be reviewed and fixed. 15 | - type: input 16 | id: version-php-os 17 | attributes: 18 | label: PHP & Platform 19 | description: Exact PHP and Platform (OS) versions using this package. 20 | placeholder: 8.1.2 - Ubuntu 22.04 x64 21 | validations: 22 | required: true 23 | - type: input 24 | id: version-db 25 | attributes: 26 | label: Database 27 | description: Exact DB version using this package, if applicable. 28 | placeholder: MySQL 8.0.28 29 | validations: 30 | required: false 31 | - type: input 32 | id: version-laravel 33 | attributes: 34 | label: Laravel version 35 | description: Exact Laravel version using this package. 36 | placeholder: 9.2.3 37 | validations: 38 | required: true 39 | - type: checkboxes 40 | id: requirements 41 | attributes: 42 | label: Have you done this? 43 | options: 44 | - label: I have checked my logs and I'm sure is a bug in this package. 45 | required: true 46 | - label: I can reproduce this bug in isolation (vanilla Laravel install) 47 | required: true 48 | - label: I can suggest a workaround as a Pull Request 49 | required: false 50 | - type: textarea 51 | id: expectation 52 | attributes: 53 | label: Expectation 54 | description: Write what you expect to (correctly) happen. 55 | placeholder: When I do this, I expect to happen that. 56 | validations: 57 | required: true 58 | - type: textarea 59 | id: description 60 | attributes: 61 | label: Description 62 | description: Write what (incorrectly) happens instead. 63 | placeholder: Instead, when I do this, I receive that. 64 | validations: 65 | required: true 66 | - type: textarea 67 | id: reproduction 68 | attributes: 69 | label: Reproduction 70 | description: Paste the code to assert in a test, or just comment with the repository with the bug to download. 71 | render: php 72 | placeholder: | 73 | $test = something()->causedAn()->earthQuake(); 74 | 75 | $this->assertFalse($test); 76 | 77 | // or comment with "https://github.com/my-name/my-bug-report" 78 | validations: 79 | required: true 80 | - type: textarea 81 | id: logs 82 | attributes: 83 | label: Stack trace & logs 84 | description: If you have a **full** stack trace, you can copy it here. You may hide sensible information. 85 | placeholder: This is automatically formatted into code, no need for ``` backticks. 86 | render: shell 87 | validations: 88 | required: false 89 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: | 3 | Recommend a feature for this package. 4 | title: "[X.x] I recommend this new feature for this package" 5 | labels: ["enhancement"] 6 | assignees: 7 | - ikechukwukalu 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: | 12 | Thanks for contributing to this package! 13 | New features keep this package fresh and fun for everybody to use. 14 | - type: checkboxes 15 | id: requirements 16 | attributes: 17 | label: Please check these requirements 18 | options: 19 | - label: This feature helps everyone using this package 20 | required: true 21 | - label: It's feasible and maintainable 22 | required: true 23 | - label: It's non breaking 24 | required: true 25 | - label: I issued a PR with the implementation (optional) 26 | required: false 27 | - type: textarea 28 | id: description 29 | attributes: 30 | label: Description 31 | description: Describe how the feature works 32 | placeholder: | 33 | This new feature would accomplish... 34 | 35 | It could be implemented by doing... 36 | 37 | And it would be cool because... 38 | validations: 39 | required: true 40 | - type: textarea 41 | id: sample 42 | attributes: 43 | label: Code sample 44 | description: Sample a small snippet on how the feature works 45 | placeholder: | 46 | RequirePin::newFeature()->requireSomethingElse(); 47 | render: php 48 | validations: 49 | required: true 50 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 18 | 19 | # Description 20 | 21 | This feature/fix allows to... 22 | 23 | # Code samples 24 | 25 | ```php 26 | Route::requirePin(); 27 | ``` 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "09:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/requirepin.yml: -------------------------------------------------------------------------------- 1 | name: RequirePin Unit Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | 9 | byte_level: 10 | name: "Byte-level" 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - name: "Checkout code" 14 | uses: "actions/checkout@v3" 15 | - name: "Check file permissions" 16 | run: | 17 | test "$(find . -type f -not -path './.git/*' -executable)" == "" 18 | - name: "Find non-printable ASCII characters" 19 | run: | 20 | ! LC_ALL=C.UTF-8 find ./src -type f -name "*.php" -print0 | xargs -0 -- grep -PHn "[^ -~]" 21 | syntax_errors: 22 | name: "Syntax errors" 23 | runs-on: "ubuntu-latest" 24 | steps: 25 | - name: "Set up PHP" 26 | uses: "shivammathur/setup-php@v2" 27 | with: 28 | php-version: "8.2" 29 | tools: "parallel-lint" 30 | - name: "Checkout code" 31 | uses: "actions/checkout@v3" 32 | - name: "Validate Composer configuration" 33 | run: "composer validate --strict" 34 | - name: "Check source code for syntax errors" 35 | run: "composer exec -- parallel-lint src/" 36 | unit_tests: 37 | name: "Unit Tests" 38 | needs: 39 | - "byte_level" 40 | - "syntax_errors" 41 | runs-on: "ubuntu-latest" 42 | steps: 43 | - name: Setup PHP 44 | uses: shivammathur/setup-php@master 45 | with: 46 | php-version: 8.2 47 | extension-csv: mbstring, bcmath 48 | - name: "Checkout code" 49 | uses: "actions/checkout@v3" 50 | - name: Composer install 51 | run: composer install 52 | - name: "Execute unit tests" 53 | run: "composer run-script test" 54 | - name: Run Snyk to check for vulnerabilities 55 | uses: snyk/actions/php@master 56 | env: 57 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 58 | with: 59 | args: --all-projects 60 | command: test 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /vendor 3 | /.idea 4 | .php-cs-fixer.cache 5 | .phpunit.result.cache 6 | composer.lock 7 | phpunit.xml.bak 8 | /node_modules 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v2.0.2 2 | 3 | - Fixed test 4 | 5 | ## v2.0.1 6 | 7 | - Updated doctrine/dbal package to support dependencies 8 | 9 | ## v2.0.0 10 | 11 | - Updated package to support Laravel 11 12 | 13 | ## v1.0.6 14 | 15 | - Updated package to support Laravel 8 16 | - Added settings for custom auth guard - `middleware('auth_route_guard')` and `Auth::guard()->check('auth_guard')` 17 | - Fixed middleware bug 18 | - Fixed max trial bug 19 | 20 | ## v1.0.5 21 | 22 | - Corrected typos 23 | 24 | ## v1.0.4 25 | 26 | - Return correct status code from server for ajax requests 27 | 28 | ## v1.0.3 29 | 30 | - Removed static functions and switched to facade for middleware class 31 | 32 | ## v1.0.2 33 | 34 | - Removed `Books` migration file 35 | 36 | ## v1.0.1 37 | 38 | - Removed `BookController` controller class 39 | - Removed `Book` model class 40 | - Removed `createBookRequest` request class 41 | - Removed `php artisan sample:routes` command 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Ikechukwu Kalu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RequirePin 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/ikechukwukalu/requirepin?style=flat-square)](https://packagist.org/packages/ikechukwukalu/requirepin) 4 | [![Quality Score](https://img.shields.io/scrutinizer/quality/g/ikechukwukalu/requirepin/main?style=flat-square)](https://scrutinizer-ci.com/g/ikechukwukalu/requirepin/) 5 | [![Code Quality](https://img.shields.io/codefactor/grade/github/ikechukwukalu/requirepin?style=flat-square)](https://www.codefactor.io/repository/github/ikechukwukalu/requirepin) 6 | [![Known Vulnerabilities](https://snyk.io/test/github/ikechukwukalu/requirepin/badge.svg?style=flat-square)](https://security.snyk.io/package/composer/ikechukwukalu%2Frequirepin) 7 | [![Github Workflow Status](https://img.shields.io/github/actions/workflow/status/ikechukwukalu/requirepin/requirepin.yml?branch=main&style=flat-square)](https://github.com/ikechukwukalu/requirepin/actions/workflows/requirepin.yml) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/ikechukwukalu/requirepin?style=flat-square)](https://packagist.org/packages/ikechukwukalu/requirepin) 9 | [![GitHub Repo stars](https://img.shields.io/github/stars/ikechukwukalu/requirepin?style=flat-square)](https://github.com/ikechukwukalu/requirepin/stargazers) 10 | [![GitHub issues](https://img.shields.io/github/issues/ikechukwukalu/requirepin?style=flat-square)](https://github.com/ikechukwukalu/requirepin/issues) 11 | [![GitHub forks](https://img.shields.io/github/forks/ikechukwukalu/requirepin?style=flat-square)](https://github.com/ikechukwukalu/requirepin/forks) 12 | [![Licence](https://img.shields.io/packagist/l/ikechukwukalu/requirepin?style=flat-square)](https://github.com/ikechukwukalu/requirepin/blob/main/LICENSE.md) 13 | 14 | **RequirePin** is a Laravel package that provides middleware to enforce PIN confirmation and validation before processing requests to specified routes, adding an extra layer of security to your application. 15 | 16 | ## Table of Contents 17 | 18 | - [Requirements](#requirements) 19 | - [Installation](#installation) 20 | - [Configuration](#configuration) 21 | - [Usage](#usage) 22 | - [Applying Middleware](#applying-middleware) 23 | - [Routes](#routes) 24 | - [Customization](#customization) 25 | - [Publishing Configuration](#publishing-configuration) 26 | - [Publishing Language Files](#publishing-language-files) 27 | - [Publishing Views](#publishing-views) 28 | - [Reserved Keys for Payload](#reserved-keys-for-payload) 29 | - [To Display Return Payload Within Blade](#to-display-return-payload-within-blade) 30 | - [Security Considerations](#security-considerations) 31 | - [Contributing](#contributing) 32 | - [License](#license) 33 | 34 | ## Requirements 35 | 36 | - PHP 7.3 or higher 37 | - Laravel 8 or higher 38 | 39 | ## Installation 40 | 41 | To install the package, run the following command: 42 | 43 | ```bash 44 | composer require ikechukwukalu/requirepin 45 | ``` 46 | 47 | After installation, publish the migration files: 48 | 49 | ```bash 50 | php artisan vendor:publish --tag=rp-migrations 51 | ``` 52 | 53 | Then, run the migrations: 54 | 55 | ```bash 56 | php artisan migrate 57 | ``` 58 | 59 | Configure your `.env` file to use Redis for queue management: 60 | 61 | ```env 62 | REDIS_CLIENT=predis 63 | QUEUE_CONNECTION=redis 64 | ``` 65 | 66 | Finally, start the queue worker: 67 | 68 | ```bash 69 | php artisan queue:work 70 | ``` 71 | 72 | ## Configuration 73 | 74 | **RequirePin** uses Redis to manage PIN confirmation queues efficiently. Ensure that your Redis server is properly configured and running. 75 | 76 | ## Usage 77 | 78 | ### Applying Middleware 79 | 80 | To enforce PIN confirmation on specific routes, apply the `require.pin` middleware to those routes or route groups. For example: 81 | 82 | ```php 83 | Route::middleware(['require.pin'])->group(function () { 84 | // Protected routes 85 | }); 86 | ``` 87 | 88 | ### Routes 89 | 90 | The package provides the following routes: 91 | 92 | **API Routes:** 93 | 94 | - `POST api/change/pin`: Endpoint to change the user's PIN. 95 | - `POST api/pin/required/{uuid}`: Endpoint to confirm the PIN for a specific request. 96 | 97 | **Web Routes:** 98 | 99 | - `POST change/pin`: Endpoint to change the user's PIN. 100 | - `POST pin/required/{uuid}`: Endpoint to confirm the PIN for a specific request. 101 | - `GET change/pin`: Page to display the form for changing the PIN. 102 | - `GET pin/required/{uuid?}`: Page to display the form for PIN confirmation. 103 | 104 | **Note:** To receive JSON responses, add the `'Accept: application/json'` header to your requests. 105 | 106 | ## Reserved Keys for Payload 107 | 108 | The following keys are reserved for use within the payload: 109 | 110 | - `uuid` - Unique identifier for the PIN request. 111 | - `pin` - The PIN value submitted by the user. 112 | - `expires` - Expiration time for the PIN request. 113 | - `signature` - Timestamp indicating when the PIN was verified. 114 | - `return_payload` 115 | - `pin_validation` 116 | 117 | Ensure these keys are not overridden when handling the payload. 118 | 119 | ## To Display Return Payload Within Blade 120 | 121 | To display the returned payload values within a Blade template, use: 122 | 123 | ```blade 124 | @if (session('return_payload')) 125 | @php 126 | [$status, $status_code, $data] = json_decode(session('return_payload'), true); 127 | @endphp 128 |
129 | {!! $data['message'] !!} 130 |
131 | @endif 132 | ``` 133 | 134 | You can customize this based on your application's needs. 135 | 136 | ## Security Considerations 137 | 138 | - **PIN Policies:** Ensure that your application enforces strong PIN policies, such as minimum length and complexity requirements. 139 | - **Rate Limiting:** Implement rate limiting on PIN confirmation endpoints to prevent brute-force attacks. 140 | - **Secure Storage:** Store PINs securely using appropriate hashing algorithms. 141 | 142 | ## Contributing 143 | 144 | Contributions are welcome! Please read the [contribution guidelines](CONTRIBUTING.md) before submitting a pull request. 145 | 146 | ## License 147 | 148 | This package is open-sourced software licensed under the [MIT license](LICENSE.md). 149 | -------------------------------------------------------------------------------- /READMEOLD.md: -------------------------------------------------------------------------------- 1 | # REQUIRE PIN 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/ikechukwukalu/requirepin?style=flat-square)](https://packagist.org/packages/ikechukwukalu/requirepin) 4 | [![Quality Score](https://img.shields.io/scrutinizer/quality/g/ikechukwukalu/requirepin/main?style=flat-square)](https://scrutinizer-ci.com/g/ikechukwukalu/requirepin/) 5 | [![Code Quality](https://img.shields.io/codefactor/grade/github/ikechukwukalu/requirepin?style=flat-square)](https://www.codefactor.io/repository/github/ikechukwukalu/requirepin) 6 | [![Known Vulnerabilities](https://snyk.io/test/github/ikechukwukalu/requirepin/badge.svg?style=flat-square)](https://security.snyk.io/package/composer/ikechukwukalu%2Frequirepin) 7 | [![Github Workflow Status](https://img.shields.io/github/actions/workflow/status/ikechukwukalu/requirepin/requirepin.yml?branch=main&style=flat-square)](https://github.com/ikechukwukalu/requirepin/actions/workflows/requirepin.yml) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/ikechukwukalu/requirepin?style=flat-square)](https://packagist.org/packages/ikechukwukalu/requirepin) 9 | [![GitHub Repo stars](https://img.shields.io/github/stars/ikechukwukalu/requirepin?style=flat-square)](https://github.com/ikechukwukalu/requirepin/stargazers) 10 | [![GitHub issues](https://img.shields.io/github/issues/ikechukwukalu/requirepin?style=flat-square)](https://github.com/ikechukwukalu/requirepin/issues) 11 | [![GitHub forks](https://img.shields.io/github/forks/ikechukwukalu/requirepin?style=flat-square)](https://github.com/ikechukwukalu/requirepin/forks) 12 | [![Licence](https://img.shields.io/packagist/l/ikechukwukalu/requirepin?style=flat-square)](https://github.com/ikechukwukalu/requirepin/blob/main/LICENSE.md) 13 | 14 | A simple Laravel package that provides a middleware which will require users to confirm routes utilizing their pin for authentication. 15 | 16 | ## REQUIREMENTS 17 | 18 | - PHP 7.3+ 19 | - Laravel 8+ 20 | 21 | ## STEPS TO INSTALL 22 | 23 | ``` shell 24 | composer require ikechukwukalu/requirepin 25 | ``` 26 | 27 | - `php artisan vendor:publish --tag=rp-migrations` 28 | - `php artisan migrate` 29 | - Set `REDIS_CLIENT=predis` and `QUEUE_CONNECTION=redis` within your `.env` file. 30 | - `php artisan queue:work` 31 | 32 | ## ROUTES 33 | 34 | ### Api routes 35 | 36 | - **POST** `api/change/pin` 37 | - **POST** `api/pin/required/{uuid}` 38 | 39 | ### Web routes 40 | 41 | - **POST** `change/pin` 42 | - **POST** `pin/required/{uuid}` 43 | - **GET** `change/pin` 44 | - **GET** `pin/required/{uuid?}` 45 | 46 | ## NOTE 47 | 48 | - To receive json response add `'Accept': 'application/json'` to your headers. 49 | 50 | ## HOW IT WORKS 51 | 52 | - First, it's like eating candy. 53 | - The `require.pin` middlware should be added to a route or route group. 54 | - This middleware will arrest all incoming requests. 55 | - A temporary URL (`pin/required/{uuid}`) is generated for a user to authenticate with the specified input `config(requirepin.input)` using their pin. 56 | - It either returns a `JSON` response with the generated URL or it redirects to a page where a user is required to authenticate the request by entering their pin into a form that will send a **POST** request to the generated URL when submitted. 57 | - To display return payload within blade: 58 | 59 | ```js 60 | @if (session('return_payload')) 61 | @php 62 | [$status, $status_code, $data] = json_decode(session('return_payload'), true); 63 | @endphp 64 |
65 | {!! $data['message'] !!} 66 |
67 | @endif 68 | ``` 69 | 70 | ### Reserved keys for payload 71 | 72 | - `_uuid` 73 | - `_pin` 74 | - `expires` 75 | - `signature` 76 | - `return_payload` 77 | - `pin_validation` 78 | 79 | ## PUBLISH CONFIG 80 | 81 | - `php artisan vendor:publish --tag=rp-config` 82 | 83 | ## PUBLISH LANG 84 | 85 | - `php artisan vendor:publish --tag=rp-lang` 86 | 87 | ## PUBLISH VIEWS 88 | 89 | - `php artisan vendor:publish --tag=rp-views` 90 | 91 | ## LICENSE 92 | 93 | The RP package is an open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). 94 | -------------------------------------------------------------------------------- /Tests/CommandTest.php: -------------------------------------------------------------------------------- 1 | artisan('vendor:publish --tag=rp-config')->assertSuccessful(); 14 | 15 | $this->artisan('vendor:publish --tag=rp-migrations')->assertSuccessful(); 16 | 17 | $this->artisan('vendor:publish --tag=rp-lang')->assertSuccessful(); 18 | 19 | $this->artisan('vendor:publish --tag=rp-views')->assertSuccessful(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/Controllers/BookController.php: -------------------------------------------------------------------------------- 1 | validated(); 34 | 35 | if ($book = Book::create($validated)) { 36 | $data = $book; 37 | return $this->httpResponse($request, trans('requirepin::general.success'), 200, $data); 38 | } 39 | 40 | $data = ['message' => 'Book could not be created']; 41 | return $this->httpResponse($request, trans('requirepin::general.fail'), 500, $data); 42 | } 43 | 44 | /** 45 | * Delete Book. 46 | * 47 | * @param Illuminate\Http\Request $request 48 | * @param int $id 49 | * 50 | * @return \Illuminate\Http\JsonResponse 51 | * @return \Illuminate\Http\RedirectResponse 52 | * @return \Illuminate\Http\Response 53 | */ 54 | public function deleteBook(Request $request, int $id): JsonResponse|RedirectResponse|Response 55 | { 56 | if (Book::where('id', $id)->delete()) { 57 | $data = Book::withTrashed()->find($id); 58 | return $this->httpResponse($request, trans('requirepin::general.success'), 200, $data); 59 | } 60 | 61 | $data = ['message' => 'Book could not be deleted']; 62 | return $this->httpResponse($request, trans('requirepin::general.fail'), 500, $data); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/Models/Book.php: -------------------------------------------------------------------------------- 1 | 'date', 25 | ]; 26 | 27 | public function setReleaseDateAttribute($value) { 28 | return $this->attributes['release_date'] = date('Y-m-d', strtotime($value)); 29 | } 30 | 31 | public function getReleaseDateAttribute($value) { 32 | return date('Y-m-d', strtotime($value)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/NotificationsTest.php: -------------------------------------------------------------------------------- 1 | str::random(), 24 | 'email' => Str::random(40) . '@example.com', 25 | 'password' => Hash::make('password'), 26 | 'pin' => Hash::make('0000'), 27 | ]); 28 | 29 | $this->actingAs($user); 30 | $user->notify(new PinChange()); 31 | 32 | Notification::assertSentTo( 33 | [$user], PinChange::class 34 | ); 35 | 36 | Notification::assertCount(1); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/PinTest.php: -------------------------------------------------------------------------------- 1 | str::random(), 31 | 'email' => Str::random(40) . '@example.com', 32 | 'password' => Hash::make('password'), 33 | 'pin' => Hash::make(config('requirepin.default', '0000')) 34 | ]); // Would still have the default pin 35 | 36 | $this->actingAs($user); 37 | 38 | $postData = [ 39 | 'current_pin' => '9090', //Wrong current pin 40 | 'pin' => '1uu4', //Wrong pin format 41 | 'pin_confirmation' => '1234' //None matching pins 42 | ]; 43 | 44 | $response = $this->post('/test/change/pin', $postData, ['Accept' => 'application/json']); 45 | $responseArray = json_decode($response->getContent(), true); 46 | 47 | $this->assertTrue(isset($responseArray['message'])); 48 | $this->assertTrue(isset($responseArray['errors'])); 49 | 50 | $response = $this->post('/test/change/pin', $postData); 51 | $responseArray = json_decode($response->getContent(), true); 52 | 53 | $this->assertEquals(302, $response->status()); 54 | } 55 | 56 | public function testChangePin() 57 | { 58 | $user = TestUser::create([ 59 | 'name' => str::random(), 60 | 'email' => Str::random(40) . '@example.com', 61 | 'password' => Hash::make('password'), 62 | 'pin' => Hash::make(config('requirepin.default', '0000')), 63 | ]); 64 | 65 | $this->actingAs($user); 66 | 67 | $postData = [ 68 | 'current_pin' => config('requirepin.default', '0000'), 69 | 'pin' => '1234', 70 | 'pin_confirmation' => '1234' 71 | ]; 72 | 73 | $this->assertTrue(Hash::check($postData['current_pin'], $user->pin)); 74 | 75 | $response = $this->post('/test/change/pin', $postData, ['Accept' => 'application/json']); 76 | $responseArray = json_decode($response->getContent(), true); 77 | 78 | $this->assertEquals(200, $responseArray['status_code']); 79 | $this->assertEquals( 'success', $responseArray['status']); 80 | } 81 | 82 | public function testRequirePinMiddleWareForCreateBookWeb() 83 | { 84 | $user = TestUser::create([ 85 | 'name' => str::random(), 86 | 'email' => Str::random(40) . '@example.com', 87 | 'password' => Hash::make('password'), 88 | 'pin' => Hash::make('1234'), 89 | 'default_pin' => 0 90 | ]); 91 | 92 | $this->actingAs($user); 93 | 94 | $this->assertTrue(Hash::check('1234', $user->pin)); 95 | 96 | $postData = [ 97 | 'name' => $this->faker->sentence(rand(1,5)), 98 | 'isbn' => $this->faker->unique()->isbn13(), 99 | 'authors' => implode(",", [$this->faker->name(), $this->faker->name()]), 100 | 'publisher' => $this->faker->name(), 101 | 'number_of_pages' => rand(45,1500), 102 | 'country' => $this->faker->countryISOAlpha3(), 103 | 'release_date' => date('Y-m-d') 104 | ]; 105 | 106 | $response = $this->post(route('createBookTest'), $postData); 107 | $this->assertEquals(302, $response->status()); 108 | } 109 | 110 | public function testRequirePinMiddleWareForDeleteBookWeb() 111 | { 112 | $user = TestUser::create([ 113 | 'name' => str::random(), 114 | 'email' => Str::random(40) . '@example.com', 115 | 'password' => Hash::make('password'), 116 | 'pin' => Hash::make('1234'), 117 | 'default_pin' => 0 118 | ]); 119 | 120 | $this->actingAs($user); 121 | 122 | $this->assertTrue(Hash::check('1234', $user->pin)); 123 | 124 | $book = Book::find(1); 125 | 126 | if (!isset($book->id)) { 127 | $book = Book::create([ 128 | 'name' => $this->faker->sentence(rand(1,5)), 129 | 'isbn' => $this->faker->unique()->isbn13(), 130 | 'authors' => implode(",", [$this->faker->name(), $this->faker->name()]), 131 | 'publisher' => $this->faker->name(), 132 | 'number_of_pages' => rand(45,1500), 133 | 'country' => $this->faker->countryISOAlpha3(), 134 | 'release_date' => date('Y-m-d') 135 | ]); 136 | } 137 | 138 | $id = $book->id; 139 | 140 | $response = $this->json('DELETE', route('deleteBookTest', ['id' => $id])); 141 | $this->assertEquals(200, $response->status()); 142 | } 143 | 144 | public function testRequirePinMiddleWareForDeleteBook() 145 | { 146 | $user = TestUser::create([ 147 | 'name' => str::random(), 148 | 'email' => Str::random(40) . '@example.com', 149 | 'password' => Hash::make('password'), 150 | 'pin' => Hash::make('1234'), 151 | 'default_pin' => 0 152 | ]); 153 | 154 | $this->actingAs($user); 155 | 156 | $this->assertTrue(Hash::check('1234', $user->pin)); 157 | 158 | $book = Book::find(1); 159 | 160 | if (!isset($book->id)) { 161 | $book = Book::create([ 162 | 'name' => $this->faker->sentence(rand(1,5)), 163 | 'isbn' => $this->faker->unique()->isbn13(), 164 | 'authors' => implode(",", [$this->faker->name(), $this->faker->name()]), 165 | 'publisher' => $this->faker->name(), 166 | 'number_of_pages' => rand(45,1500), 167 | 'country' => $this->faker->countryISOAlpha3(), 168 | 'release_date' => date('Y-m-d') 169 | ]); 170 | } 171 | 172 | $id = $book->id; 173 | 174 | $response = $this->json('DELETE', route('deleteBookTest', ['id' => $id]), ['Accept' => 'application/json']); 175 | $responseArray = json_decode($response->getContent(), true); 176 | 177 | $this->assertEquals(200, $responseArray['status_code']); 178 | $this->assertEquals('success', $responseArray['status']); 179 | $this->assertTrue(isset($responseArray['data']['url'])); 180 | 181 | $postData = [ 182 | config('requirepin.input', '_pin') => '1234' 183 | ]; 184 | $url = $responseArray['data']['url']; 185 | 186 | $response = $this->post($url, $postData, ['Accept' => 'application/json']); 187 | $responseArray = json_decode($response->getContent(), true); 188 | 189 | $this->assertEquals(200, $responseArray['status_code']); 190 | $this->assertEquals('success', $responseArray['status']); 191 | } 192 | 193 | public function testRequirePinMiddleWareForCreateBook() 194 | { 195 | $user = TestUser::create([ 196 | 'name' => str::random(), 197 | 'email' => Str::random(40) . '@example.com', 198 | 'password' => Hash::make('password'), 199 | 'pin' => Hash::make('1234'), 200 | 'default_pin' => 0 201 | ]); 202 | 203 | $this->actingAs($user); 204 | 205 | $this->assertTrue(Hash::check('1234', $user->pin)); 206 | 207 | $postData = [ 208 | 'name' => $this->faker->sentence(rand(1,5)), 209 | 'isbn' => $this->faker->unique()->isbn13(), 210 | 'authors' => implode(",", [$this->faker->name(), $this->faker->name()]), 211 | 'publisher' => $this->faker->name(), 212 | 'number_of_pages' => rand(45,1500), 213 | 'country' => $this->faker->countryISOAlpha3(), 214 | 'release_date' => date('Y-m-d') 215 | ]; 216 | 217 | $response = $this->post(route('createBookTest'), $postData, ['Accept' => 'application/json']); 218 | $responseArray = json_decode($response->getContent(), true); 219 | 220 | $this->assertEquals(200, $responseArray['status_code']); 221 | $this->assertEquals('success', $responseArray['status']); 222 | $this->assertTrue(isset($responseArray['data']['url'])); 223 | 224 | $postData = [ 225 | config('requirepin.input', '_pin') => '1234' 226 | ]; 227 | $url = $responseArray['data']['url']; 228 | 229 | $response = $this->post($url, $postData, ['Accept' => 'application/json']); 230 | $responseArray = json_decode($response->getContent(), true); 231 | 232 | $this->assertEquals(200, $responseArray['status_code']); 233 | $this->assertEquals('success', $responseArray['status']); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /Tests/Requests/CreateBookRequest.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function rules(): array 23 | { 24 | return [ 25 | 'name' => 'required|min:6|max:100', 26 | 'isbn' => 'required|min:6|max:100|unique:books', 27 | 'authors' => 'required|min:6|max:1000', 28 | 'country' => 'required|max:100', 29 | 'number_of_pages' => 'required|digits_between:1,5', 30 | 'publisher' => 'required|min:6|max:100', 31 | 'release_date' => 'required|date', 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/ServiceProviderTest.php: -------------------------------------------------------------------------------- 1 | app->make('files') 14 | ->getRequire(RequirePinServiceProvider::CONFIG), 15 | $this->app->make('config')->get('require-pin') 16 | ); 17 | } 18 | 19 | public function test_loads_translations(): void 20 | { 21 | static::assertArrayHasKey('requirepin', 22 | $this->app->make('translator')->getLoader()->namespaces()); 23 | } 24 | 25 | public function test_publishes_middleware(): void 26 | { 27 | $middleware = $this->app->make('router')->getMiddleware(); 28 | 29 | static::assertSame(RequirePin::class, $middleware['require.pin']); 30 | static::assertArrayHasKey('require.pin', $middleware); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Tests/TestCase.php: -------------------------------------------------------------------------------- 1 | loadLaravelMigrations(); 24 | $this->loadMigrationsFrom(__DIR__.'/migrations'); 25 | } 26 | 27 | protected function defineRoutes($router) 28 | { 29 | // Define routes. 30 | $router->get('login', function () { 31 | return 'login'; 32 | })->name('login'); 33 | 34 | $router->post('test/v1/sample/books', [BookController::class, 'createBook']) 35 | ->name('createBookTest') 36 | ->middleware('require.pin'); 37 | 38 | $router->delete('test/v1/sample/books/{id}', [BookController::class, 'deleteBook']) 39 | ->name('deleteBookTest') 40 | ->middleware('require.pin'); 41 | 42 | $router->post('/test/change/pin', [PinController::class, 'changePin']) 43 | ->name('changePin'); 44 | 45 | $router->post('/test/pin/required/{uuid}', [PinController::class, 46 | 'pinRequired'])->name('pinRequired'); 47 | } 48 | 49 | protected function getPackageProviders($app): array 50 | { 51 | return [RequirePinServiceProvider::class, 52 | LocationServiceProvider::class]; 53 | } 54 | 55 | protected function getEnvironmentSetUp($app) { 56 | $app['config']->set('auth.guards.sanctum', [ 57 | 'driver' => 'session', 58 | 'provider' => 'users', 59 | ]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/migrations/0001_01_01_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('email')->unique(); 18 | $table->timestamp('email_verified_at')->nullable(); 19 | $table->string('password'); 20 | $table->rememberToken(); 21 | $table->timestamps(); 22 | }); 23 | 24 | Schema::create('password_reset_tokens', function (Blueprint $table) { 25 | $table->string('email')->primary(); 26 | $table->string('token'); 27 | $table->timestamp('created_at')->nullable(); 28 | }); 29 | 30 | Schema::create('sessions', function (Blueprint $table) { 31 | $table->string('id')->primary(); 32 | $table->foreignId('user_id')->nullable()->index(); 33 | $table->string('ip_address', 45)->nullable(); 34 | $table->text('user_agent')->nullable(); 35 | $table->longText('payload'); 36 | $table->integer('last_activity')->index(); 37 | }); 38 | } 39 | 40 | /** 41 | * Reverse the migrations. 42 | */ 43 | public function down(): void 44 | { 45 | Schema::dropIfExists('users'); 46 | Schema::dropIfExists('password_reset_tokens'); 47 | Schema::dropIfExists('sessions'); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /Tests/migrations/2022_09_12_005402_create_books_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name'); 19 | $table->string('isbn')->unique(); 20 | $table->longText('authors'); 21 | $table->string('country'); 22 | $table->integer('number_of_pages'); 23 | $table->string('publisher'); 24 | $table->date('release_date'); 25 | $table->softDeletes(); 26 | $table->timestamps(); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('books'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ikechukwukalu/requirepin", 3 | "description": "A laravel package for pin confirmation and validation before processing requests to a specified route", 4 | "type": "library", 5 | "license": "MIT", 6 | "autoload": { 7 | "psr-4": { 8 | "Ikechukwukalu\\Requirepin\\": "src/", 9 | "Ikechukwukalu\\Requirepin\\Controllers\\": "src/Controllers/", 10 | "Ikechukwukalu\\Requirepin\\Facades\\": "src/Facades/", 11 | "Ikechukwukalu\\Requirepin\\Middleware\\": "src/Middleware/", 12 | "Ikechukwukalu\\Requirepin\\Models\\": "src/Models/", 13 | "Ikechukwukalu\\Requirepin\\Notifications\\": "src/Notifications/", 14 | "Ikechukwukalu\\Requirepin\\Requests\\": "src/Requests/", 15 | "Ikechukwukalu\\Requirepin\\Rules\\": "src/Rules/", 16 | "Ikechukwukalu\\Requirepin\\Services\\": "src/Services/", 17 | "Ikechukwukalu\\Requirepin\\Traits\\": "src/Traits/" 18 | } 19 | }, 20 | "autoload-dev": { 21 | "psr-4": { 22 | "Ikechukwukalu\\Requirepin\\Tests\\": "Tests/", 23 | "Ikechukwukalu\\Requirepin\\Tests\\Controllers\\": "Tests/Controllers/", 24 | "Ikechukwukalu\\Requirepin\\Tests\\Models\\": "Tests/Models/", 25 | "Ikechukwukalu\\Requirepin\\Tests\\Requests\\": "Tests/Requests/" 26 | } 27 | }, 28 | "scripts": { 29 | "test": "vendor/bin/phpunit", 30 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 31 | }, 32 | "authors": [ 33 | { 34 | "name": "Ikechukwu Kalu", 35 | "email": "ea.ikechukwukalu@gmail.com" 36 | } 37 | ], 38 | "minimum-stability": "dev", 39 | "require": { 40 | "doctrine/dbal": "^3.1|^4.0", 41 | "illuminate/auth": "^8.0|^9.0|^10.0|^11.0", 42 | "illuminate/bus": "^8.0|^9.0|^10.0|^11.0", 43 | "illuminate/broadcasting": "^8.0|^9.0|^10.0|^11.0", 44 | "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0", 45 | "illuminate/database": "^8.0|^9.0|^10.0|^11.0", 46 | "illuminate/events": "^8.0|^9.0|^10.0|^11.0", 47 | "illuminate/http": "^8.0|^9.0|^10.0|^11.0", 48 | "illuminate/notifications": "^8.0|^9.0|^10.0|^11.0", 49 | "illuminate/queue": "^8.0|^9.0|^10.0|^11.0", 50 | "illuminate/routing": "^8.0|^9.0|^10.0|^11.0", 51 | "illuminate/support": "^8.0|^9.0|^10.0|^11.0", 52 | "illuminate/validation": "^8.0|^9.0|^10.0|^11.0", 53 | "illuminate/view": "^8.0|^9.0|^10.0|^11.0", 54 | "laravel/sanctum": "^2.8|^3.2|^4.0", 55 | "laravel/ui": "^3.1|^4.4", 56 | "php": ">=7.3", 57 | "predis/predis": "^2.0", 58 | "stevebauman/location": "^6.6|^7.0", 59 | "symfony/http-foundation": "^5.4|^6.0|^7.0" 60 | }, 61 | "require-dev": { 62 | "mockery/mockery": "^1.0|^2.0", 63 | "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0", 64 | "phpunit/phpunit": "^9.0|^10.0|^11.0", 65 | "php-parallel-lint/php-parallel-lint": "dev-develop" 66 | }, 67 | "extra": { 68 | "laravel": { 69 | "providers": [ 70 | "Ikechukwukalu\\Requirepin\\RequirePinServiceProvider" 71 | ] 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /coverage.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikechukwukalu/requirepin/66525a1b7311430f49f890b96a767be44bc8c393/coverage.xml -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "requirepin", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "requirepin", 9 | "version": "1.0.0", 10 | "license": "ISC" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "requirepin", 3 | "version": "1.0.0", 4 | "description": "Laravel pin authentication", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/ikechukwukalu/requirepin.git" 12 | }, 13 | "author": "ikechukwukalu", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/ikechukwukalu/requirepin/issues" 17 | }, 18 | "homepage": "https://github.com/ikechukwukalu/requirepin#readme" 19 | } 20 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | src/ 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikechukwukalu/requirepin/66525a1b7311430f49f890b96a767be44bc8c393/src/.DS_Store -------------------------------------------------------------------------------- /src/Controllers/PinController.php: -------------------------------------------------------------------------------- 1 | pinService->handlePinChange($request)) 38 | { 39 | return $this->httpResponse($request, 40 | trans('requirepin::general.success'), 200, $data); 41 | } 42 | 43 | return $this->unknownErrorResponse($request); 44 | } 45 | 46 | /** 47 | * Pin Authentication. 48 | * 49 | * @param \Illuminate\Http\Request $request 50 | * 51 | * @return \Illuminate\Http\JsonResponse 52 | * @return \Illuminate\Http\RedirectResponse 53 | * @return \Illuminate\Http\Response 54 | */ 55 | public function pinRequired(Request $request, string $uuid): JsonResponse|RedirectResponse|Response 56 | { 57 | if ($data = $this->pinService->pinRequestAttempts($request, $uuid)) { 58 | return $this->pinService->errorResponseForPinRequired($request, 59 | $uuid, 500, $data); 60 | } 61 | 62 | if ($data = $this->pinService->pinUrlHasValidSignature($request)) { 63 | return $this->pinService->errorResponseForPinRequired($request, 64 | $uuid, 400, $data); 65 | } 66 | 67 | if ($data = $this->pinService->pinUrlHasValidUUID($uuid)) { 68 | return $this->pinService->errorResponseForPinRequired($request, 69 | $uuid, 401, $data); 70 | } 71 | 72 | if ($data = $this->pinService->pinValidation($request)) { 73 | return $this->pinService->errorResponseForPinRequired($request, 74 | $uuid, 400, $data); 75 | } 76 | 77 | return $this->pinService->handlePinRequired($request, $uuid); 78 | } 79 | 80 | /** 81 | * Change Pin View. 82 | * 83 | * @return \Illuminate\View\View 84 | */ 85 | public function changePinView(): View 86 | { 87 | return view('requirepin::pin.changepin'); 88 | } 89 | 90 | /** 91 | * Require Pin View. 92 | * 93 | * @return \Illuminate\View\View 94 | */ 95 | public function requirePinView(): View 96 | { 97 | return view('requirepin::pin.pinrequired'); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Facades/PinService.php: -------------------------------------------------------------------------------- 1 | check()) 20 | { 21 | return $this->httpResponse( 22 | ...$pinService::pinRequestTerminated($request)); 23 | } 24 | 25 | if ($request->has(config('requirepin.param', '_uuid')) 26 | && $pinService::isArrestedRequestValid($request)) 27 | { 28 | return $next($request); 29 | } 30 | 31 | $pinService::cancelAllOpenArrestedRequests(); 32 | 33 | return $pinService::requirePinValidationForRequest($request, 34 | $this->getUserIp($request)); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/Models/OldPin.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Models/RequirePin.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Models/TestUser.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | protected $fillable = [ 22 | 'name', 23 | 'email', 24 | 'password', 25 | 'pin', 26 | 'default_pin', 27 | ]; 28 | 29 | /** 30 | * The attributes that should be hidden for serialization. 31 | * 32 | * @var array 33 | */ 34 | protected $hidden = [ 35 | 'password', 36 | 'remember_token', 37 | ]; 38 | 39 | /** 40 | * The attributes that should be cast. 41 | * 42 | * @var array 43 | */ 44 | protected $casts = [ 45 | 'email_verified_at' => 'datetime', 46 | ]; 47 | } 48 | -------------------------------------------------------------------------------- /src/Models/User.php: -------------------------------------------------------------------------------- 1 | subject(trans('requirepin::pin.notify.subject')) 23 | ->line(trans('requirepin::pin.notify.introduction')) 24 | ->line(trans('requirepin::pin.notify.message')) 25 | ->action(trans('requirepin::pin.notify.action'), url(config('requirepin.change_pin_route'))) 26 | ->line(trans('requirepin::pin.notify.complimentary_close')); 27 | } 28 | 29 | public function toArray($notifiable) 30 | { 31 | return [ 32 | // 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Requests/ChangePinRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', new CurrentPin(true)], 28 | 'pin' => [ 29 | 'required', 'string', 30 | 'max:' . config('requirepin.max', 4), 31 | Password::min(config('requirepin.min', 4))->numbers(), 32 | 'confirmed', 33 | new DisallowOldPin( 34 | config('requirepin.check_all', true), 35 | config('requirepin.number', 4) 36 | ) 37 | ], 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/RequirePinServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->make(Router::class); 31 | $router->aliasMiddleware('require.pin', RequirePin::class); 32 | 33 | Route::middleware('api')->prefix('api')->group(function () { 34 | $this->loadRoutesFrom(static::ROUTE_API); 35 | }); 36 | 37 | Route::middleware('web')->group(function () { 38 | $this->loadRoutesFrom(static::ROUTE_WEB); 39 | }); 40 | 41 | $this->loadMigrationsFrom(static::DB); 42 | $this->loadViewsFrom(static::VIEW, 'requirepin'); 43 | $this->loadTranslationsFrom(static::LANG, 'requirepin'); 44 | 45 | $this->publishes([ 46 | static::CONFIG => config_path('requirepin.php'), 47 | ], 'rp-config'); 48 | $this->publishes([ 49 | static::DB => database_path('migrations'), 50 | ], 'rp-migrations'); 51 | $this->publishes([ 52 | static::LANG => lang_path('vendor/requirepin'), 53 | ], 'rp-lang'); 54 | $this->publishes([ 55 | static::VIEW => resource_path('views/vendor/requirepin'), 56 | ], 'rp-views'); 57 | } 58 | 59 | /** 60 | * Register the application services. 61 | * 62 | * @return void 63 | */ 64 | public function register() 65 | { 66 | $this->mergeConfigFrom( 67 | static::CONFIG, 'require-pin' 68 | ); 69 | 70 | $this->app->make(\Ikechukwukalu\Requirepin\Controllers\PinController::class); 71 | 72 | $this->app->bind(ThrottleRequestsService::class, function (Application $app) { 73 | return new ThrottleRequestsService( 74 | config('sanctumauthstarter.login.maxAttempts', 3), 75 | config('sanctumauthstarter.login.delayMinutes', 1) 76 | ); 77 | }); 78 | 79 | $this->app->bind('PinService', PinService::class); 80 | 81 | $appConfig = Config::get('app'); 82 | $packageFacades = [ 83 | 'PinService' => \Ikechukwukalu\Clamavfileupload\Facades\Foundation\PinService::class, 84 | ]; 85 | $appConfig['aliases'] = array_merge($appConfig['aliases'], $packageFacades); 86 | Config::set('app', $appConfig); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Rules/CurrentPin.php: -------------------------------------------------------------------------------- 1 | allowDefaultPin = $allowDefaultPin; 23 | } 24 | 25 | /** 26 | * Determine if the validation rule passes. 27 | * 28 | * @param string $attribute 29 | * @param mixed $value 30 | * @return bool 31 | */ 32 | public function passes($attribute, $value) 33 | { 34 | if (Auth::guard(config('requirepin.auth_guard', 'web'))->user()->default_pin && !$this->allowDefaultPin) { 35 | $this->defaultPin = true; 36 | 37 | return false; 38 | } 39 | 40 | return Hash::check($value, Auth::guard(config('requirepin.auth_guard', 'web'))->user()->pin); 41 | } 42 | 43 | /** 44 | * Get the validation error message. 45 | * 46 | * @return string 47 | */ 48 | public function message() 49 | { 50 | if ($this->defaultPin) { 51 | return trans('requirepin::pin.default'); 52 | } 53 | 54 | return trans('requirepin::pin.wrong'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Rules/DisallowOldPin.php: -------------------------------------------------------------------------------- 1 | checkAll = $checkAll; 27 | $this->number = $number; 28 | 29 | if (is_int($this->checkAll) && !empty($this->checkAll)) { 30 | $this->number = $checkAll; 31 | } 32 | 33 | $this->user = Auth::guard(config('requirepin.auth_guard', 'web'))->user(); 34 | } 35 | 36 | /** 37 | * Determine if the validation rule passes. 38 | * 39 | * @param string $attribute 40 | * @param mixed $value 41 | * @return bool 42 | */ 43 | public function passes($attribute, $value) 44 | { 45 | $oldpins = $this->getOldPins(); 46 | 47 | if ((string) $value === (string) config('requirepin.default', '0000')) 48 | { 49 | return false; 50 | } 51 | 52 | if ($oldpins->count() === 0) { 53 | return !Hash::check($value, $this->user->pin); 54 | } 55 | 56 | foreach ($oldpins as $oldpin) { 57 | if (Hash::check($value, $oldpin->pin)) { 58 | return false; 59 | } 60 | } 61 | 62 | return true; 63 | } 64 | 65 | /** 66 | * Get the validation error message. 67 | * 68 | * @return string 69 | */ 70 | public function message() 71 | { 72 | return trans_choice('requirepin::pin.exists', 73 | intval(is_int($this->checkAll)), 74 | ['number' => $this->number]); 75 | } 76 | 77 | /** 78 | * Get OldPin Model. 79 | * 80 | * @return \Illuminate\Database\Eloquent\Collection 81 | */ 82 | private function getOldPins(): EloquentCollection 83 | { 84 | if ($this->checkAll === true) { 85 | return OldPin::where('user_id', $this->user->id) 86 | ->orderBy('created_at', 'desc') 87 | ->get(); 88 | } 89 | 90 | return OldPin::where('user_id', $this->user->id) 91 | ->orderBy('created_at', 'desc') 92 | ->take($this->number) 93 | ->get(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Services/PinService.php: -------------------------------------------------------------------------------- 1 | throttleRequestsService = new ThrottleRequestsService( 37 | config('requirepin.max_attempts', 3), 38 | config('requirepin.delay_minutes', 1) 39 | ); 40 | } 41 | 42 | /** 43 | * Handle Pin Change. 44 | * 45 | * @param \Ikechukwukalu\Requirepin\Requests\ChangePinRequest $request 46 | * 47 | * @return null 48 | * @return array 49 | */ 50 | public function handlePinChange(ChangePinRequest $request) : ?array 51 | { 52 | $validated = $request->validated(); 53 | 54 | if ($user = $this->saveNewPin($validated)) { 55 | $this->saveOldPin($user, $validated); 56 | $this->sendPinChangeNotification($user); 57 | 58 | return ['message' => trans('requirepin::pin.changed')]; 59 | } 60 | 61 | return null; 62 | } 63 | 64 | /** 65 | * Handle Pin Authentication. 66 | * 67 | * @param \Illuminate\Http\Request $request 68 | * @param string $uuid 69 | * 70 | * @return \Illuminate\Http\JsonResponse 71 | * @return \Illuminate\Http\RedirectResponse 72 | * @return \Illuminate\Http\Response 73 | */ 74 | public function handlePinRequired(Request $request, string $uuid): JsonResponse|Response|RedirectResponse 75 | { 76 | if (!$requirePin = $this->getRequirePin($uuid)) { 77 | $this->transferSessionsToNewRequest($request); 78 | 79 | return $this->shouldResponseBeJson($request) 80 | ? $this->httpResponse($request, 81 | trans('requirepin::general.fail'), 400, 82 | ['message' => trans('requirepin::pin.unknown_error')] 83 | ) 84 | : redirect($requirePin->redirect_to)->with('return_payload', 85 | session('return_payload')); 86 | } 87 | 88 | $this->throttleRequestsService->clearAttempts($request); 89 | 90 | $this->updateCurrentRequest($request, $requirePin); 91 | $response = $this->dispatchArrestedRequest($request, 92 | $requirePin, $uuid); 93 | $this->transferSessionsToNewRequest($request); 94 | 95 | $requirePin->approved_at = now(); 96 | $requirePin->save(); 97 | 98 | if (session('return_payload')) { 99 | return redirect($requirePin->redirect_to)->with('return_payload', 100 | session('return_payload')); 101 | } 102 | 103 | return $response; 104 | } 105 | 106 | /** 107 | * Pin Request Attempts. 108 | * 109 | * @param \Illuminate\Http\Request $request 110 | * 111 | * @return null 112 | * @return array 113 | */ 114 | public function pinRequestAttempts(Request $request, string $uuid): ?array 115 | { 116 | $response = $this->requestAttempts($request, 'requirepin::pin.throttle'); 117 | 118 | if ($response) { 119 | $requirePin = $this->getRequirePin($uuid); 120 | 121 | if(!$requirePin) { 122 | return ['message' => 123 | trans('requirepin::pin.invalid_url')]; 124 | } 125 | 126 | $this->checkMaxTrial($requirePin); 127 | } 128 | 129 | return $response; 130 | } 131 | 132 | /** 133 | * Valid Pin URL. 134 | * 135 | * @param \Illuminate\Http\Request $request 136 | * 137 | * @return null 138 | * @return array 139 | */ 140 | public function pinUrlHasValidSignature(Request $request): ?array 141 | { 142 | if (!$request->hasValidSignature()) { 143 | return ['message' => 144 | trans('requirepin::pin.expired_url')]; 145 | } 146 | 147 | return null; 148 | } 149 | 150 | /** 151 | * Valid UUID For Pin URL. 152 | * 153 | * @param string $uuid 154 | * 155 | * @return null 156 | * @return array 157 | */ 158 | public function pinUrlHasValidUUID(string $uuid): ?array 159 | { 160 | if(!$this->getRequirePin($uuid)) { 161 | return ['message' => 162 | trans('requirepin::pin.invalid_url')]; 163 | } 164 | 165 | return null; 166 | } 167 | 168 | /** 169 | * Pin Validation. 170 | * 171 | * @param \Illuminate\Http\Request $request 172 | * 173 | * @return null 174 | * @return array 175 | */ 176 | public function pinValidation(Request $request): ?array 177 | { 178 | $validator = Validator::make($request->all(), [ 179 | config('requirepin.input', '_pin') => ['required', 'string', 180 | new CurrentPin(config('requirepin.allow_default_pin', false))] 181 | ]); 182 | 183 | if ($validator->fails()) { 184 | return ['message' => $validator->errors()->first()]; 185 | } 186 | 187 | return null; 188 | } 189 | 190 | /** 191 | * Get RequirePin Model. 192 | * 193 | * @param string $uuid 194 | * 195 | * @return null 196 | * @return \Ikechukwukalu\Requirepin\Models\RequirePin 197 | */ 198 | public function getRequirePin(string $uuid): ?RequirePin 199 | { 200 | return RequirePin::where('user_id', Auth::guard(config('requirepin.auth_guard', 'web'))->user()->id) 201 | ->where('uuid', $uuid) 202 | ->whereNull('approved_at') 203 | ->whereNull('cancelled_at') 204 | ->first(); 205 | } 206 | 207 | /** 208 | * Error Response For Pin Authentication. 209 | * 210 | * @param \Illuminate\Http\Request $request 211 | * @param string $uuid 212 | * @param int $status_code 213 | * @param array $data 214 | * 215 | * @return \Illuminate\Http\JsonResponse 216 | * @return \Illuminate\Http\RedirectResponse 217 | * @return \Illuminate\Http\Response 218 | */ 219 | public function errorResponseForPinRequired(Request $request, string $uuid, int $status_code, array $data): JsonResponse|RedirectResponse|Response 220 | { 221 | if ($this->shouldResponseBeJson($request)) { 222 | return $this->httpResponse($request, 223 | trans('requirepin::general.fail'), $status_code, $data); 224 | } 225 | 226 | $requirePin = $this->getRequirePin($uuid); 227 | 228 | if (isset($requirePin->pin_validation_url)) { 229 | return back()->with('pin_validation', 230 | json_encode([$data['message'], 231 | $requirePin->pin_validation_url, $status_code])); 232 | } 233 | 234 | return back()->with('pin_validation', 235 | json_encode([trans('requirepin::pin.unknown_error'), 236 | 'javascript:void(0)', '500'])); 237 | } 238 | 239 | /** 240 | * Pin Request Terminated. 241 | * 242 | * @return array 243 | */ 244 | public function pinRequestTerminated(Request $request): array 245 | { 246 | return [$request, trans('requirepin::general.fail'), 401, 247 | ['message' => trans('requirepin::pin.terminated')]]; 248 | } 249 | 250 | /** 251 | * Error Response For Pin Authentication. 252 | * 253 | * @param \Illuminate\Http\Request $request 254 | * 255 | * @return bool 256 | */ 257 | public function isArrestedRequestValid(Request $request): bool 258 | { 259 | $param = config('requirepin.param', '_uuid'); 260 | $requirePin = RequirePin::where('user_id', Auth::guard(config('requirepin.auth_guard', 'web'))->user()->id) 261 | ->where('route_arrested', $request->path()) 262 | ->where('uuid', $request->{$param}) 263 | ->whereNull('approved_at') 264 | ->whereNull('cancelled_at') 265 | ->first(); 266 | 267 | if (!isset($requirePin->id)) { 268 | return false; 269 | } 270 | 271 | return true; 272 | } 273 | 274 | /** 275 | * Cancel Unprocessed Arrested Request. 276 | * 277 | * @return void 278 | */ 279 | public function cancelAllOpenArrestedRequests(): void 280 | { 281 | RequirePin::where('user_id', Auth::guard(config('requirepin.auth_guard', 'web'))->user()->id) 282 | ->whereNull('approved_at') 283 | ->whereNull('cancelled_at') 284 | ->update(['cancelled_at' => now()]); 285 | } 286 | 287 | /** 288 | * Pin Validation For RequirePin Middleware. 289 | * 290 | * @param \Illuminate\Http\Request $request 291 | * @param string $ip 292 | * 293 | * @return \Illuminate\Http\JsonResponse 294 | * @return \Illuminate\Http\RedirectResponse 295 | * @return \Illuminate\Http\Response 296 | */ 297 | public function requirePinValidationForRequest(Request $request, string $ip): JsonResponse|RedirectResponse|Response 298 | { 299 | $arrestRouteData = $this->arrestRequest($request, $ip); 300 | [$status, $status_code, $data] = $this->pinValidationURL(...$arrestRouteData); 301 | 302 | if ($this->shouldResponseBeJson($request)) 303 | { 304 | return ResponseFacade::json([ 305 | 'status' => $status, 306 | 'status_code' => $status_code, 307 | 'data' => $data 308 | ]); 309 | } 310 | 311 | return redirect(route('requirePinView'))->with('pin_validation', 312 | json_encode([$data['message'], $data['url'], $status_code])); 313 | } 314 | 315 | /** 316 | * Arrest Request. 317 | * 318 | * @param \Illuminate\Http\Request $request 319 | * @param string $ip 320 | * 321 | * @return array 322 | */ 323 | private function arrestRequest(Request $request, string $ip): array 324 | { 325 | $redirect_to = url()->previous() ?? '/'; 326 | $uuid = (string) Str::uuid(); 327 | $expires_at = now()->addSeconds(config('requirepin.duration', 328 | null)); 329 | $pin_validation_url = URL::temporarySignedRoute( 330 | $this->pinRequiredRoute($request), $expires_at, ['uuid' => $uuid]); 331 | 332 | RequirePin::create([ 333 | "user_id" => Auth::guard(config('requirepin.auth_guard', 'web'))->user()->id, 334 | "uuid" => $uuid, 335 | "ip" => $ip, 336 | "device" => $request->userAgent(), 337 | "method" => $request->method(), 338 | "route_arrested" => $request->path(), 339 | "payload" => Crypt::encryptString(serialize($request->all())), 340 | "redirect_to" => $redirect_to, 341 | "pin_validation_url" => $pin_validation_url, 342 | "expires_at" => $expires_at 343 | ]); 344 | 345 | return [$pin_validation_url, $redirect_to]; 346 | } 347 | 348 | /** 349 | * Pin Validation URL. 350 | * 351 | * @param string $url 352 | * @param null|string $redirect 353 | * 354 | * @return array 355 | */ 356 | private function pinValidationURL(string $url, null|string $redirect): array 357 | { 358 | return [trans('requirepin::general.success'), 200, 359 | [ 360 | 'message' => trans('requirepin::pin.require_pin'), 361 | 'url' => $url, 362 | 'redirect' => $redirect 363 | ]]; 364 | } 365 | 366 | /** 367 | * Dispatch Arrested Request. 368 | * 369 | * @param \Illuminate\Http\Request $request 370 | * @param \Ikechukwukalu\Requirepin\Models\RequirePin $requirePin 371 | * @param string $uuid 372 | * 373 | * @return \Illuminate\Http\JsonResponse 374 | * @return \Illuminate\Http\RedirectResponse 375 | * @return \Illuminate\Http\Response 376 | */ 377 | private function dispatchArrestedRequest(Request $request, RequirePin $requirePin, string $uuid): JsonResponse|RedirectResponse|Response 378 | { 379 | $this->arrestedRequest = Request::create($requirePin->route_arrested, 380 | $requirePin->method, ['_uuid' => $uuid] + $this->payload); 381 | 382 | if ($this->shouldResponseBeJson($request)) { 383 | $this->arrestedRequest->headers->set('Accept', 'application/json'); 384 | } 385 | 386 | return Route::dispatch($this->arrestedRequest); 387 | } 388 | 389 | /** 390 | * Transfer Sessions To New Request. 391 | * 392 | * @param \Illuminate\Http\Request $request 393 | * 394 | * @return void 395 | */ 396 | private function transferSessionsToNewRequest(Request $request): void 397 | { 398 | if ($this->shouldResponseBeJson($request)) 399 | { 400 | return; 401 | } 402 | 403 | foreach ($this->arrestedRequest->session()->all() as $key => $session) { 404 | if (!in_array($key, ['_old_input', '_previous', 'errors'])) { 405 | continue; 406 | } 407 | 408 | $request->session()->flash($key, $session); 409 | } 410 | } 411 | 412 | /** 413 | * Update Current Request. 414 | * 415 | * @param \Illuminate\Http\Request $request 416 | * @param \Ikechukwukalu\Requirepin\Models\RequirePin $requirePin 417 | * 418 | * @return void 419 | */ 420 | private function updateCurrentRequest(Request $request, RequirePin $requirePin): void 421 | { 422 | $this->payload = unserialize(Crypt::decryptString($requirePin->payload)); 423 | 424 | $request->merge([ 425 | 'expires' => null, 426 | 'signature' => null, 427 | config('requirepin.input', '_pin') => null 428 | ]); 429 | 430 | foreach($this->payload as $key => $item) { 431 | $request->merge([$key => $item]); 432 | } 433 | } 434 | 435 | /** 436 | * Save User's New Pin. 437 | * 438 | * @param array $validated 439 | * 440 | * @return null 441 | * @return \App\Models\User 442 | */ 443 | private function saveNewPin(array $validated) 444 | { 445 | $user = Auth::guard(config('requirepin.auth_guard', 'web'))->user(); 446 | $user->pin = Hash::make($validated['pin']); 447 | $user->default_pin = (string) $validated['pin'] === (string) config('requirepin.default', '0000'); 448 | 449 | if ($user->save()) { 450 | return $user; 451 | } 452 | 453 | return null; 454 | } 455 | 456 | /** 457 | * Save User's Old Pin. 458 | * 459 | * @param \App\Models\User $user 460 | * @param array $validated 461 | * 462 | * @return void 463 | */ 464 | private function saveOldPin(User|TestUser $user, array $validated): void 465 | { 466 | OldPin::create([ 467 | 'user_id' => $user->id, 468 | 'pin' => Hash::make($validated['current_pin']) 469 | ]); 470 | } 471 | 472 | /** 473 | * Send Pin change Notification. 474 | * 475 | * @param \App\Models\User $user 476 | * 477 | * @return void 478 | */ 479 | private function sendPinChangeNotification(User|TestUser $user): void 480 | { 481 | if (config('requirepin.notify.change', true) 482 | && env('APP_ENV') !== 'package_testing') { 483 | $user->notify(new PinChange()); 484 | } 485 | } 486 | 487 | /** 488 | * Max Route Dispatch For Arrested Request. 489 | * 490 | * @param \Ikechukwukalu\Requirepin\Models\RequirePin $requirePin 491 | * 492 | * @return void 493 | */ 494 | private function checkMaxTrial(RequirePin $requirePin): void 495 | { 496 | $maxTrial = $requirePin->retry + 1; 497 | 498 | if ($maxTrial >= config('requirepin.max_trial', 3)) { 499 | $requirePin->cancelled_at = now(); 500 | } 501 | 502 | $requirePin->retry = $maxTrial; 503 | $requirePin->save(); 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /src/Services/ThrottleRequestsService.php: -------------------------------------------------------------------------------- 1 | maxAttempts = $maxAttempts; 17 | $this->delayMinutes = $delayMinutes; 18 | } 19 | 20 | public function hasTooManyAttempts (Request $request) 21 | { 22 | return $this->hasTooManyLoginAttempts($request); 23 | } 24 | 25 | public function incrementAttempts (Request $request) 26 | { 27 | return $this->incrementLoginAttempts($request); 28 | } 29 | 30 | public function clearAttempts (Request $request) 31 | { 32 | return $this->clearLoginAttempts($request); 33 | } 34 | 35 | public function _fireLockoutEvent (Request $request) 36 | { 37 | return $this->fireLockoutEvent($request); 38 | } 39 | 40 | public function _limiter () 41 | { 42 | return $this->limiter(); 43 | } 44 | 45 | public function _throttleKey (Request $request) 46 | { 47 | return $this->throttleKey($request); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Traits/Helpers.php: -------------------------------------------------------------------------------- 1 | throttleRequestsService = new ThrottleRequestsService( 20 | config('requirepin.login.max_attempts', 3), 21 | config('requirepin.login.delay_minutes', 1) 22 | ); 23 | } 24 | 25 | /** 26 | * HTTP Response. 27 | * 28 | * @param \Illuminate\Http\Request $request 29 | * @param string $status 30 | * @param int $status_code 31 | * @param array $data 32 | * 33 | * @return \Illuminate\Http\JsonResponse 34 | * @return \Illuminate\Http\RedirectResponse 35 | * @return \Illuminate\Http\Response 36 | */ 37 | public function httpResponse(Request $request, string $status, int $status_code, $data = null): RedirectResponse|JsonResponse|Response 38 | { 39 | if ($this->shouldResponseBeJson($request)) { 40 | return ResponseFacade::json([ 41 | 'status' => $status, 42 | 'status_code' => $status_code, 43 | 'data' => $data 44 | ], $status_code); 45 | } 46 | 47 | return back()->with('return_payload', json_encode([ 48 | $status, $status_code, $data])); 49 | } 50 | 51 | /** 52 | * Get User IP. 53 | * 54 | * @param \Illuminate\Http\Request $request 55 | * 56 | * @return string 57 | */ 58 | public function getUserIp(Request $request): string 59 | { 60 | if ($position = Location::get()) { 61 | return $position->ip; 62 | } 63 | 64 | $server_keys = [ 65 | 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 66 | 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 67 | 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 68 | 'REMOTE_ADDR' 69 | ]; 70 | 71 | foreach ($server_keys as $key){ 72 | if (array_key_exists($key, $_SERVER) === true) { 73 | foreach (explode(',', $_SERVER[$key]) as $ip) { 74 | $ip = trim($ip); // just to be safe 75 | 76 | if (filter_var($ip, FILTER_VALIDATE_IP, 77 | FILTER_FLAG_NO_PRIV_RANGE | 78 | FILTER_FLAG_NO_RES_RANGE) !== false 79 | ) { 80 | return $ip; 81 | } 82 | } 83 | } 84 | } 85 | 86 | return $request->ip(); // it will return server ip when no client ip found 87 | } 88 | 89 | /** 90 | * Unknown Error Response. 91 | * 92 | * @param \Illuminate\Http\Request $request 93 | * 94 | * @return \Illuminate\Http\JsonResponse 95 | * @return \Illuminate\Http\RedirectResponse 96 | * @return \Illuminate\Http\Response 97 | */ 98 | public function unknownErrorResponse(Request $request): RedirectResponse|JsonResponse|Response 99 | { 100 | $data = ['message' => 101 | trans('requirepin::general.unknown_error')]; 102 | 103 | return $this->httpResponse($request, 104 | trans('requirepin::general.fail'), 422, $data); 105 | } 106 | 107 | /** 108 | * HTTP Response. 109 | * 110 | * @param \Illuminate\Http\Request $request 111 | * @param string $trans 112 | * 113 | * @return null 114 | * @return array 115 | */ 116 | public function requestAttempts(Request $request, string $trans = 'requirepin::auth.throttle'): ?array 117 | { 118 | if ($this->throttleRequestsService->hasTooManyAttempts($request)) { 119 | $this->throttleRequestsService->_fireLockoutEvent($request); 120 | 121 | return ["message" => trans($trans, 122 | ['seconds' => 123 | $this->throttleRequestsService->_limiter() 124 | ->availableIn( 125 | $this->throttleRequestsService 126 | ->_throttleKey($request) 127 | ) 128 | ]) 129 | ]; 130 | } 131 | 132 | $this->throttleRequestsService->incrementAttempts($request); 133 | 134 | return null; 135 | } 136 | 137 | public function shouldResponseBeJson(Request $request): bool 138 | { 139 | return $request->wantsJson() || $request->ajax(); 140 | } 141 | 142 | public function pinRequiredRoute(Request $request): string 143 | { 144 | $prefix = explode('/', $request->route()->getPrefix())[0]; 145 | 146 | if ($prefix === 'api' || $prefix === 'test') { 147 | return 'pinRequired'; 148 | } 149 | 150 | return 'pinRequiredWeb'; 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /src/config/requirepin.php: -------------------------------------------------------------------------------- 1 | '0000', 8 | /** 9 | * bool - Allow a user to authenticate using the default pin 10 | */ 11 | 'allow_default_pin' => false, 12 | /** 13 | * int - Uses seconds. Make sure to update the 'expires_at' 14 | * column if you changed this value after migration 15 | */ 16 | 'duration' => 300, 17 | /** 18 | * boolean 19 | */ 20 | 'verify_sender' => true, 21 | /** 22 | * string - Name of form input 23 | */ 24 | 'input' => '_pin', 25 | /** 26 | * string - Name of URL param 27 | */ 28 | 'param' => '_uuid', 29 | /** 30 | * int - Max chars for pin 31 | */ 32 | 'max' => 4, 33 | /** 34 | * int - Min chars for pin 35 | */ 36 | 'min' => 4, 37 | /** 38 | * int|boolean - Check all or a specified number of 39 | * previous pins 40 | */ 41 | 'check_all' => true, 42 | /** 43 | * int - Number of previous pins to check 44 | */ 45 | 'number' => 4, 46 | /** 47 | * int - Pin authentication rate limit 48 | */ 49 | 'max_attempts' => 3, 50 | /** 51 | * int - Number of minutes a user is supposed to wait before 52 | * another attempt 53 | */ 54 | 'delay_minutes' => 1, 55 | /** 56 | * int - Number of times a user is allowed to try and authenticate 57 | * before the route is cancelled 58 | */ 59 | 'max_trial' => 3, 60 | /** 61 | * string - Route that will be displayed in the notification 62 | * that is sent when a user's pin has been changed 63 | */ 64 | 'change_pin_route' => 'change/pin', 65 | 66 | /** 67 | * Pin notification configurations 68 | */ 69 | 'notify' => [ 70 | /** 71 | * boolean - Send a notification whenever pin is changed 72 | */ 73 | 'change' => true, 74 | ], 75 | 76 | /** 77 | * string - Route that will be displayed in the notification 78 | * that is sent when a user's pin has been changed 79 | */ 80 | 'auth_route_guard' => 'auth', 81 | 82 | /** 83 | * string - Route that will be displayed in the notification 84 | * that is sent when a user's pin has been changed 85 | */ 86 | 'auth_guard' => 'web', 87 | 88 | /** 89 | * sanctum/api - Route middleware of authenticated user 90 | */ 91 | 'auth_middleware' => 'sanctum', 92 | ]; 93 | -------------------------------------------------------------------------------- /src/lang/en/general.php: -------------------------------------------------------------------------------- 1 | 'success', 5 | 'fail' => 'fail', 6 | 'no_changes' => 'Nothing to change', 7 | 'unknown_error' => 'An error has occurred', 8 | 'not_found' => 'Not Found', 9 | 10 | ]; 11 | -------------------------------------------------------------------------------- /src/lang/en/pin.php: -------------------------------------------------------------------------------- 1 | 'You have to change the current pin. You are not allowed to use the default pin!', 5 | 'require_pin' => 'Enter your pin to proceed!', 6 | 'terminated' => 'User is not logged in!', 7 | 'expired_url' => 'URL has expired!', 8 | 'invalid_url' => 'URL is invalid!', 9 | 'changed' => 'Your pin has been changed!', 10 | 'wrong' => 'You\'ve entered the wrong pin!', 11 | 'throttle' => 'Too many attempts. Please try again in :seconds seconds.', 12 | 'unverified_sender' => 'Sender cannot be verified!', 13 | 'unknown_error' => 'You are not allowed to make anymore attempts for this request', 14 | 'exists' => '{0} You are not allowed to use the existing pin or any previous pin!|{1} You are not allowed to use any of your last :number pins or the existing pin!', 15 | 'notify' => [ 16 | 'subject' => 'Your pin was changed.', 17 | 'introduction' => 'Your pin has been changed successfully!', 18 | 'message' => 'If this action was not carried out by you, please click the link below to change your pin.', 19 | 'action' => 'Click Here', 20 | 'complimentary_close' => 'Take care!' 21 | ], 22 | 'not_allowed' => 'Not allowed!', 23 | 24 | ]; 25 | -------------------------------------------------------------------------------- /src/migrations/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikechukwukalu/requirepin/66525a1b7311430f49f890b96a767be44bc8c393/src/migrations/.DS_Store -------------------------------------------------------------------------------- /src/migrations/2022_10_11_234205_add_column_pin_to_users_table.php: -------------------------------------------------------------------------------- 1 | string('pin')->after('password')->default(Hash::make(config('requirepin.default', '0000'))); 19 | $table->tinyInteger('default_pin')->after('pin')->default(1); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::table('users', function (Blueprint $table) { 31 | $table->dropColumn(['pin', 'default_pin']); 32 | }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/migrations/2022_10_12_193200_create_require_pins_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignId('user_id')->constrained('users'); 19 | $table->string('uuid')->unique(); 20 | $table->string('ip'); 21 | $table->string('device'); 22 | $table->string('method'); 23 | $table->text('route_arrested'); 24 | $table->longText('payload'); 25 | $table->text('redirect_to')->nullable(); 26 | $table->text('pin_validation_url'); 27 | $table->tinyInteger('retry')->default(0); 28 | $table->timestamp('approved_at', $precision = 0)->nullable(); 29 | $table->timestamp('cancelled_at', $precision = 0)->nullable(); 30 | // $table->timestamp('expires_at')->default( \DB::raw("DATE_ADD(now(), INTERVAL " . config('requirepin.duration', 300) . " SECOND)")); //Works fine 31 | $table->timestamp('expires_at')->nullable(); 32 | $table->timestamps(); 33 | }); 34 | } 35 | 36 | /** 37 | * Reverse the migrations. 38 | * 39 | * @return void 40 | */ 41 | public function down() 42 | { 43 | Schema::dropIfExists('require_pins'); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/migrations/2022_10_12_224623_create_old_pins_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignId('user_id')->constrained('users'); 19 | $table->string('pin'); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::dropIfExists('old_pins'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/routes/api.php: -------------------------------------------------------------------------------- 1 | group(function () { 9 | Route::post('change/pin', [PinController::class, 'changePin']) 10 | ->name('changePin'); 11 | 12 | Route::post('pin/required/{uuid}', [PinController::class, 'pinRequired']) 13 | ->name('pinRequired'); 14 | }); 15 | -------------------------------------------------------------------------------- /src/routes/web.php: -------------------------------------------------------------------------------- 1 | group(function () { 7 | Route::post('change/pin', [PinController::class, 'changePin']) 8 | ->name('changePinWeb'); 9 | 10 | Route::post('pin/required/{uuid}', [PinController::class, 'pinRequired']) 11 | ->name('pinRequiredWeb'); 12 | 13 | Route::get('change/pin', [PinController::class, 'changePinView']) 14 | ->name('changePinView'); 15 | 16 | Route::get('pin/required/{uuid?}', [PinController::class, 'requirePinView']) 17 | ->name('requirePinView'); 18 | }); 19 | -------------------------------------------------------------------------------- /src/views/layouts/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{ config('app.name', 'Laravel') }} 11 | 12 | 13 | 14 | 15 | 16 | 17 | @vite(['resources/sass/app.scss', 'resources/js/app.js']) 18 | 19 | 20 |
21 | 82 | 83 |
84 | @yield('content') 85 |
86 |
87 | 88 | 89 | -------------------------------------------------------------------------------- /src/views/pin/changepin.blade.php: -------------------------------------------------------------------------------- 1 | @extends('requirepin::layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |
7 |
8 |
{{ __('Change Pin') }}
9 | 10 |
11 | @if(session('return_payload')) 12 | @php 13 | [$status, $status_code, $data] = json_decode(session('return_payload'), true); 14 | @endphp 15 |
16 | {!! $data['message'] !!} 17 |
18 | @endif 19 |
20 | @csrf 21 | 22 |
23 | 24 | 25 |
26 | 27 | 28 | @error('current_pin') 29 | 30 | {{ $message }} 31 | 32 | @enderror 33 |
34 |
35 | 36 |
37 | 38 | 39 |
40 | 41 | 42 | @error('pin') 43 | 44 | {{ $message }} 45 | 46 | @enderror 47 |
48 |
49 | 50 |
51 | 52 | 53 |
54 | 55 | 56 | @error('pin_confirmation') 57 | 58 | {{ $message }} 59 | 60 | @enderror 61 |
62 |
63 | 64 |
65 |
66 | 69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | @endsection 78 | -------------------------------------------------------------------------------- /src/views/pin/pinrequired.blade.php: -------------------------------------------------------------------------------- 1 | @extends('requirepin::layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |
7 |
8 |
{{ __('Pin Required') }}
9 | 10 |
11 | @if (session('return_payload')) 12 | @php 13 | [$status, $status_code, $data] = json_decode(session('return_payload'), true); 14 | @endphp 15 |
16 | {!! $data['message'] !!} 17 |
18 | @endif 19 | @if (session('pin_validation')) 20 | @php 21 | [$message, $url, $code] = json_decode(session('pin_validation'), true); 22 | @endphp 23 |
24 | {{ $message }} 25 |
26 |
27 | @else 28 | @if (!session('return_payload')) 29 |
30 | {{ __('No pin validation url') }} 31 |
32 | @endif 33 | 34 | @endif 35 | @csrf 36 | 37 |
38 | 40 | 41 |
42 | 45 | 46 | @error('_pin') 47 | 48 | {{ $message }} 49 | 50 | @enderror 51 |
52 |
53 | 54 |
55 |
56 | 59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | @endsection 68 | --------------------------------------------------------------------------------