├── .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 | [](https://packagist.org/packages/ikechukwukalu/requirepin)
4 | [](https://scrutinizer-ci.com/g/ikechukwukalu/requirepin/)
5 | [](https://www.codefactor.io/repository/github/ikechukwukalu/requirepin)
6 | [](https://security.snyk.io/package/composer/ikechukwukalu%2Frequirepin)
7 | [](https://github.com/ikechukwukalu/requirepin/actions/workflows/requirepin.yml)
8 | [](https://packagist.org/packages/ikechukwukalu/requirepin)
9 | [](https://github.com/ikechukwukalu/requirepin/stargazers)
10 | [](https://github.com/ikechukwukalu/requirepin/issues)
11 | [](https://github.com/ikechukwukalu/requirepin/forks)
12 | [](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 | [](https://packagist.org/packages/ikechukwukalu/requirepin)
4 | [](https://scrutinizer-ci.com/g/ikechukwukalu/requirepin/)
5 | [](https://www.codefactor.io/repository/github/ikechukwukalu/requirepin)
6 | [](https://security.snyk.io/package/composer/ikechukwukalu%2Frequirepin)
7 | [](https://github.com/ikechukwukalu/requirepin/actions/workflows/requirepin.yml)
8 | [](https://packagist.org/packages/ikechukwukalu/requirepin)
9 | [](https://github.com/ikechukwukalu/requirepin/stargazers)
10 | [](https://github.com/ikechukwukalu/requirepin/issues)
11 | [](https://github.com/ikechukwukalu/requirepin/forks)
12 | [](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 |
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 |
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 |
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 |
62 |
63 |
64 |
65 |
66 |
67 | @endsection
68 |
--------------------------------------------------------------------------------