From 592c15aac25c758bf532913f9737eef461021dcc Mon Sep 17 00:00:00 2001 From: Jeremy Postlethwaite Date: Sat, 10 Feb 2024 12:50:34 -0800 Subject: [PATCH] GH-7 (#8) GH-7 --- .github/workflows/ci.yml | 217 +++++++- .gitignore | 1 + .php-cs-fixer.dist => .php-cs-fixer.dist.php | 9 +- README.md | 91 ++-- composer.json | 26 +- config/playground-auth.php | 149 ++---- .../2014_10_12_000000_create_users_table.php | 3 +- ...000_create_password_reset_tokens_table.php | 3 +- ..._08_19_000000_create_failed_jobs_table.php | 31 -- ...01_create_personal_access_tokens_table.php | 3 +- .../2014_10_12_000000_create_users_table.php | 121 +++++ ...000_create_password_reset_tokens_table.php | 28 + ...01_create_personal_access_tokens_table.php | 33 ++ lang/en/auth.php | 1 + phpstan.neon.dist | 22 + .../docs/artisan-about-playground-auth.png | Bin 54954 -> 44527 bytes resources/views/confirm-password.blade.php | 95 ---- resources/views/forgot-password.blade.php | 95 ---- resources/views/login.blade.php | 102 ---- resources/views/register.blade.php | 101 ---- resources/views/reset-password.blade.php | 93 ---- resources/views/sitemap.blade.php | 49 -- resources/views/verify-email.blade.php | 75 --- routes/auth.php | 129 ----- src/Console/Commands/HashPassword.php | 42 +- .../AuthenticatedSessionController.php | 246 --------- .../ConfirmablePasswordController.php | 59 --- src/Http/Controllers/Controller.php | 27 - .../EmailVerificationController.php | 77 --- .../Controllers/NewPasswordController.php | 84 --- .../PasswordResetLinkController.php | 56 -- .../Controllers/RegisteredUserController.php | 66 --- .../Requests/EmailVerificationRequest.php | 38 -- src/Http/Requests/LoginRequest.php | 101 ---- src/Http/Requests/PasswordResetRequest.php | 38 -- src/Issuer.php | 233 +++++++++ src/Policies/Contracts/Role.php | 39 ++ src/Policies/ModelPolicy.php | 129 +++++ src/Policies/Policy.php | 93 ++++ src/Policies/PolicyTrait.php | 85 +++ src/Policies/PrivilegeTrait.php | 139 +++++ src/Policies/RoleTrait.php | 158 ++++++ src/ServiceProvider.php | 151 ++++-- .../Controllers/AuthenticationRouteTest.php | 156 ------ .../EmailVerificationRouteTest.php | 208 -------- .../PasswordConfirmationRouteTest.php | 48 -- .../Controllers/PasswordResetRouteTest.php | 119 ----- .../Controllers/RegistrationRouteTest.php | 36 -- tests/Feature/TestCase.php | 76 +-- .../Commands/HashPassword/CommandTest.php | 52 +- .../Policies/ModelPolicy/AbstractRoleTest.php | 490 ++++++++++++++++++ .../Unit/Policies/ModelPolicy/TestPolicy.php | 14 + tests/Unit/Policies/Policy/AbstractTest.php | 159 ++++++ tests/Unit/Policies/Policy/TestPolicy.php | 14 + tests/Unit/Policies/PolicyTrait/Policy.php | 14 + tests/Unit/Policies/PolicyTrait/TraitTest.php | 138 +++++ .../PrivilegeTrait/PrivilegeModelPolicy.php | 17 + .../PrivilegeTrait/PrivilegePolicy.php | 14 + .../Policies/PrivilegeTrait/TraitTest.php | 147 ++++++ .../Policies/PrivilegeTrait/UserPolicy.php | 14 + .../Policies/RoleTrait/RoleModelPolicy.php | 17 + tests/Unit/Policies/RoleTrait/TraitTest.php | 88 ++++ tests/Unit/TestCase.php | 56 +- 63 files changed, 2724 insertions(+), 2491 deletions(-) rename .php-cs-fixer.dist => .php-cs-fixer.dist.php (98%) delete mode 100644 database/migrations-laravel/2019_08_19_000000_create_failed_jobs_table.php create mode 100644 database/migrations-playground/2014_10_12_000000_create_users_table.php create mode 100644 database/migrations-playground/2014_10_12_100000_create_password_reset_tokens_table.php create mode 100644 database/migrations-playground/2019_12_14_000001_create_personal_access_tokens_table.php create mode 100644 phpstan.neon.dist delete mode 100644 resources/views/confirm-password.blade.php delete mode 100644 resources/views/forgot-password.blade.php delete mode 100644 resources/views/login.blade.php delete mode 100644 resources/views/register.blade.php delete mode 100644 resources/views/reset-password.blade.php delete mode 100644 resources/views/sitemap.blade.php delete mode 100644 resources/views/verify-email.blade.php delete mode 100644 routes/auth.php delete mode 100644 src/Http/Controllers/AuthenticatedSessionController.php delete mode 100644 src/Http/Controllers/ConfirmablePasswordController.php delete mode 100644 src/Http/Controllers/Controller.php delete mode 100644 src/Http/Controllers/EmailVerificationController.php delete mode 100644 src/Http/Controllers/NewPasswordController.php delete mode 100644 src/Http/Controllers/PasswordResetLinkController.php delete mode 100644 src/Http/Controllers/RegisteredUserController.php delete mode 100644 src/Http/Requests/EmailVerificationRequest.php delete mode 100644 src/Http/Requests/LoginRequest.php delete mode 100644 src/Http/Requests/PasswordResetRequest.php create mode 100644 src/Issuer.php create mode 100644 src/Policies/Contracts/Role.php create mode 100644 src/Policies/ModelPolicy.php create mode 100644 src/Policies/Policy.php create mode 100644 src/Policies/PolicyTrait.php create mode 100644 src/Policies/PrivilegeTrait.php create mode 100644 src/Policies/RoleTrait.php delete mode 100644 tests/Feature/Http/Controllers/AuthenticationRouteTest.php delete mode 100644 tests/Feature/Http/Controllers/EmailVerificationRouteTest.php delete mode 100644 tests/Feature/Http/Controllers/PasswordConfirmationRouteTest.php delete mode 100644 tests/Feature/Http/Controllers/PasswordResetRouteTest.php delete mode 100644 tests/Feature/Http/Controllers/RegistrationRouteTest.php create mode 100644 tests/Unit/Policies/ModelPolicy/AbstractRoleTest.php create mode 100644 tests/Unit/Policies/ModelPolicy/TestPolicy.php create mode 100644 tests/Unit/Policies/Policy/AbstractTest.php create mode 100644 tests/Unit/Policies/Policy/TestPolicy.php create mode 100644 tests/Unit/Policies/PolicyTrait/Policy.php create mode 100644 tests/Unit/Policies/PolicyTrait/TraitTest.php create mode 100644 tests/Unit/Policies/PrivilegeTrait/PrivilegeModelPolicy.php create mode 100644 tests/Unit/Policies/PrivilegeTrait/PrivilegePolicy.php create mode 100644 tests/Unit/Policies/PrivilegeTrait/TraitTest.php create mode 100644 tests/Unit/Policies/PrivilegeTrait/UserPolicy.php create mode 100644 tests/Unit/Policies/RoleTrait/RoleModelPolicy.php create mode 100644 tests/Unit/Policies/RoleTrait/TraitTest.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c377981..94a1aaf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,12 @@ on: push: branches: [ "develop" ] pull_request: - branches: [ "develop" ] + types: + - opened + - reopened + - synchronize + - ready_for_review + - review_requested permissions: contents: write @@ -16,10 +21,74 @@ jobs: build: runs-on: ubuntu-latest - + if: ${{ github.event_name == 'push' || !github.event.pull_request.draft }} steps: - - uses: actions/checkout@v3 - # - uses: php-actions/composer@v6 + - name: Preparing timer + id: timer_start + run: | + echo "DATE_START=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT + echo "TIMESTAMP_START=$(date +'%s')" >> $GITHUB_OUTPUT + - name: "Slack notification: IN PROGRESS" + id: slack + uses: slackapi/slack-github-action@v1.24.0 + with: + channel-id: 'C068A06PV43' + payload: | + { + "text": "CI Build Status for playground-auth: IN PROGRESS\n${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": ":runner: CI Build Status for playground-auth" + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Start:*\n${{ steps.timer_start.outputs.DATE_START }}" + }, + { + "type": "mrkdwn", + "text": "*End:*\n--" + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Status:* IN PROGRESS" + }, + { + "type": "mrkdwn", + "text": ":timer_clock: --" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*PR:* ${{ github.event.pull_request.html_url || github.event.head_commit.url }}" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Build:* ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + } + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + - uses: actions/checkout@v4 - name: Run php-actions/composer@v6 uses: php-actions/composer@v6 with: @@ -30,7 +99,9 @@ jobs: env: XDEBUG_MODE: coverage with: - php_extensions: xdebug + version: "10.1" + php_version: "8.2" + php_extensions: intl xdebug coverage_clover: clover.xml coverage_text: true - name: Make code coverage badge @@ -46,3 +117,139 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} user_name: 'github-actions[bot]' user_email: 'github-actions[bot]@users.noreply.github.com' + - uses: php-actions/phpstan@v3 + with: + level: 9 + php_version: "8.2" + path: config/ database/ lang/ src/ tests/Unit/ tests/Feature/ + args: --verbose --debug + - name: Stopping timer + if: ${{ !cancelled() }} + id: timer_end + env: + TIMESTAMP_START: ${{ steps.timer_start.outputs.TIMESTAMP_START }} + run: | + echo "DATE_END=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT + echo "DURATION_PHRASE=$(($(date +'%s')-$TIMESTAMP_START)) seconds" >> $GITHUB_OUTPUT + - name: "Slack notification: Done" + uses: slackapi/slack-github-action@v1.24.0 + with: + channel-id: 'C068A06PV43' + update-ts: ${{ steps.slack.outputs.ts }} + payload: | + { + "text": "CI Build Status for playground-auth: DONE\n${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": ":white_check_mark: CI Build Status for playground-auth" + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Start:*\n${{ steps.timer_start.outputs.DATE_START }}" + }, + { + "type": "mrkdwn", + "text": "*End:*\n${{ steps.timer_end.outputs.DATE_END }}" + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Status:* Success" + }, + { + "type": "mrkdwn", + "text": ":timer_clock: ${{ steps.timer_end.outputs.DURATION_PHRASE }}" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*PR:* ${{ github.event.pull_request.html_url || github.event.head_commit.url }}" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Build:* ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + } + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + - name: "Send a notification for failures" + if: ${{ failure() }} + uses: slackapi/slack-github-action@v1.24.0 + with: + channel-id: 'C068A06PV43' + update-ts: ${{ steps.slack.outputs.ts }} + payload: | + { + "text": "CI Build Status for playground-auth: FAILED\n${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": ":warning: CI Build Status for playground-auth" + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Start:*\n${{ steps.timer_start.outputs.DATE_START }}" + }, + { + "type": "mrkdwn", + "text": "*End:*\n${{ steps.timer_end.outputs.DATE_END }}" + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Status:* FAILED" + }, + { + "type": "mrkdwn", + "text": ":timer_clock: ${{ steps.timer_end.outputs.DURATION_PHRASE }}" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*PR:* ${{ github.event.pull_request.html_url || github.event.head_commit.url }}" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Build:* ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + } + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + diff --git a/.gitignore b/.gitignore index ddd20e1..439104b 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ vendor # Test and Docs ignores ################################################################################ coverage +output phpunit.xml phpstan.neon testbench.yaml diff --git a/.php-cs-fixer.dist b/.php-cs-fixer.dist.php similarity index 98% rename from .php-cs-fixer.dist rename to .php-cs-fixer.dist.php index 1219128..5f55b1b 100644 --- a/.php-cs-fixer.dist +++ b/.php-cs-fixer.dist.php @@ -213,12 +213,11 @@ $finder = PhpCsFixer\Finder::create() ->in([ - __DIR__ . '/config', - __DIR__ . '/database', + __DIR__.'/config', + __DIR__.'/database', __DIR__ . '/lang', - __DIR__ . '/routes', - __DIR__ . '/src', - __DIR__ . '/tests/Feature', + __DIR__.'/src', + __DIR__.'/tests/Feature', __DIR__ . '/tests/Unit', ]) ->name('*.php') diff --git a/README.md b/README.md index 97153ce..41180ad 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,9 @@ [![Playground CI Workflow](https://github.com/gammamatrix/playground-auth/actions/workflows/ci.yml/badge.svg?branch=develop)](https://raw.githubusercontent.com/gammamatrix/playground-auth/testing/develop/testdox.txt) [![Test Coverage](https://raw.githubusercontent.com/gammamatrix/playground-auth/testing/develop/coverage.svg)](tests) +[![PHPStan Level 9](https://img.shields.io/badge/PHPStan-level%209-brightgreen)](.github/workflows/ci.yml#L120) -The Playground authentication package for [Laravel](https://laravel.com/docs/10.x) applications. - -This package provides endpoints and a Blade UI for handling authentication and authorization. +The Playground authentication and authorization package for [Laravel](https://laravel.com/docs/10.x) applications. More information is available [on the Playground Auth wiki.](https://github.com/gammamatrix/playground-auth/wiki) @@ -17,60 +16,51 @@ You can install the package via composer: composer require gammamatrix/playground-auth ``` +**NOTE:** This package is required by [Playground: Login Blade](https://github.com/gammamatrix/playground-login-blade) + + ## Configuration You can publish the config file with: ```bash -php artisan vendor:publish --provider="GammaMatrix\Playground\Auth\ServiceProvider" --tag="playground-config" +php artisan vendor:publish --provider="Playground\Auth\ServiceProvider" --tag="playground-config" ``` See the contents of the published config file: [config/playground-auth.php](config/playground-auth.php) -You can publish the routes file with: -```bash -php artisan vendor:publish --provider="GammaMatrix\Playground\Auth\ServiceProvider" --tag="playground-routes" -``` +The default configuration utitlizes: +- Sanctum with role based abilities +- Users may have additional abilities in the model Playground\Models\User: `users.abilities` ### Environment Variables -#### Loading +### User model types -| env() | config() | -|---------------------------------|---------------------------------| -| `PLAYGROUND_AUTH_LOAD_COMMANDS` | `playground-auth.load.commands` | -| `PLAYGROUND_LOAD_ROUTES` | `playground-auth.load.routes` | -| `PLAYGROUND_LOAD_VIEWS` | `playground-auth.load.views` | +Playground tests many different User model types to support any ecosystem. -`PLAYGROUND_LOAD_ROUTES` must be enabled to load the routes in the application (unless published to your app - the control for this is in the [ServiceProvider.php](src/ServiceProvider.php)) +Make sure your app is configured for the proper user model in the Laravel configuration: -#### Routes +```php + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => App\Models\User::class, + ], +``` -All routes are enabled by default in the base Playground Auth package. -- They may be disabled in the configuration. +During testing, Playground tests user various models. +```php +config(['auth.providers.users.model' => 'Playground\\Models\\User']) +``` -See the authentication routes: [routes/auth.php](routes/auth.php) -| env() | config() | -|-----------------------------------|-----------------------------------| -| `PLAYGROUND_AUTH_ROUTES_RESET` | `playground-auth.routes.confirm` | -| `PLAYGROUND_AUTH_ROUTES_FORGOT` | `playground-auth.routes.forgot` | -| `PLAYGROUND_AUTH_ROUTES_LOGOUT` | `playground-auth.routes.logout` | -| `PLAYGROUND_AUTH_ROUTES_LOGIN` | `playground-auth.routes.login` | -| `PLAYGROUND_AUTH_ROUTES_REGISTER` | `playground-auth.routes.register` | -| `PLAYGROUND_AUTH_ROUTES_RESET` | `playground-auth.routes.reset` | -| `PLAYGROUND_AUTH_ROUTES_TOKEN` | `playground-auth.routes.token` | -| `PLAYGROUND_AUTH_ROUTES_RESET` | `playground-auth.routes.verify` | +#### Loading -### UI +| env() | config() | +|-------------------------------------|-------------------------------------| +| `PLAYGROUND_AUTH_LOAD_COMMANDS` | `playground-auth.load.commands` | +| `PLAYGROUND_AUTH_LOAD_TRANSLATIONS` | `playground-auth.load.translations` | -| env() | config() | -|----------------------------------|----------------------------------| -| `PLAYGROUND_AUTH_LAYOUT` | `playground-auth.layout` | -| `PLAYGROUND_AUTH_VIEW` | `playground-auth.view` | -| `PLAYGROUND_AUTH_SITEMAP_ENABLE` | `playground-auth.sitemap.enable` | -| `PLAYGROUND_AUTH_SITEMAP_GUEST` | `playground-auth.sitemap.guest` | -| `PLAYGROUND_AUTH_SITEMAP_USER` | `playground-auth.sitemap.user` | -| `PLAYGROUND_AUTH_SITEMAP_VIEW` | `playground-auth.sitemap.view` | ## Commands @@ -85,17 +75,38 @@ artisan auth:hash-password 'some password' --json --pretty ``` ```json { - "password": "$2y$10$langzXKRw1GgO6VgF0IrSecqxi3gAsU5NgmmERT\/2pQXg06mSbEjS" + "hashed": "$2y$10$langzXKRw1GgO6VgF0IrSecqxi3gAsU5NgmmERT\/2pQXg06mSbEjS" } ``` +## PHPStan + +Tests at level 9 on: +- `config/` +- `database/` +- `lang/` +- `resources/views/` +- `src/` +- `tests/Feature/` +- `tests/Unit/` + +```sh +composer analyse +``` + +## Coding Standards + +```sh +composer format +``` + ## Testing ```sh composer test ``` -## About +## `artisan about` Playground Auth provides information in the `artisan about` command. diff --git a/composer.json b/composer.json index 7c574ec..28c04cc 100644 --- a/composer.json +++ b/composer.json @@ -4,11 +4,11 @@ "keywords": [ "auth", "authentication", - "blade", "gammamatrix", "laravel", "playground", "playground-auth", + "playground-login-blade", "sanctum" ], "homepage": "https://github.com/gammamatrix/playground-auth/wiki", @@ -20,16 +20,6 @@ "role": "Developer" } ], - "repositories": [ - { - "type": "vcs", - "url": "git@github.com:gammamatrix/playground.git" - }, - { - "type": "vcs", - "url": "git@github.com:gammamatrix/playground-test.git" - } - ], "require": { "php": "^8.1", "gammamatrix/playground": "dev-develop|dev-master|^73.0" @@ -41,13 +31,13 @@ "prefer-stable": true, "autoload": { "psr-4": { - "GammaMatrix\\Playground\\Auth\\": "src/" + "Playground\\Auth\\": "src/" } }, "autoload-dev": { "psr-4": { - "Tests\\Feature\\GammaMatrix\\Playground\\Auth\\": "tests/Feature/", - "Tests\\Unit\\GammaMatrix\\Playground\\Auth\\": "tests/Unit/" + "Tests\\Feature\\Playground\\Auth\\": "tests/Feature/", + "Tests\\Unit\\Playground\\Auth\\": "tests/Unit/" } }, "config": { @@ -64,13 +54,13 @@ }, "laravel": { "providers": [ - "GammaMatrix\\Playground\\Auth\\ServiceProvider" + "Playground\\Auth\\ServiceProvider" ] } }, "scripts": { - "test": "phpunit", - "format": "php-cs-fixer fix --allow-risky=yes", - "analyse": "phpstan analyse" + "test": "vendor/bin/phpunit", + "format": "vendor/bin/php-cs-fixer fix", + "analyse": "vendor/bin/phpstan analyse --verbose --debug --level max" } } diff --git a/config/playground-auth.php b/config/playground-auth.php index a19978e..acd158c 100644 --- a/config/playground-auth.php +++ b/config/playground-auth.php @@ -1,78 +1,42 @@ (string) env('PLAYGROUND_AUTH_LAYOUT', 'playground::layouts.site'), - 'view' => (string) env('PLAYGROUND_AUTH_VIEW', 'playground-auth::'), 'redirect' => env('PLAYGROUND_AUTH_REDIRECT', null), // 'session' => false, 'token' => [ - // 'expires' => 60 * 24, + // 'abilities' => '', + // 'abilities' => 'user', + 'abilities' => 'merge', 'expires' => 'tomorrow midnight', + // 'expires' => null, 'name' => 'app', // @see playground.auth.token.name + 'listed' => true, 'roles' => false, - 'privileges' => true, + 'privileges' => false, 'sanctum' => true, ], 'load' => [ 'commands' => (bool) env('PLAYGROUND_AUTH_LOAD_COMMANDS', true), - 'routes' => (bool) env('PLAYGROUND_AUTH_LOAD_ROUTES', true), - 'views' => (bool) env('PLAYGROUND_AUTH_LOAD_VIEWS', true), - ], - 'routes' => [ - 'confirm' => (bool) env('PLAYGROUND_AUTH_ROUTES_RESET', true), - 'forgot' => (bool) env('PLAYGROUND_AUTH_ROUTES_FORGOT', true), - 'logout' => (bool) env('PLAYGROUND_AUTH_ROUTES_LOGOUT', true), - 'login' => (bool) env('PLAYGROUND_AUTH_ROUTES_LOGIN', true), - 'register' => (bool) env('PLAYGROUND_AUTH_ROUTES_REGISTER', true), - 'reset' => (bool) env('PLAYGROUND_AUTH_ROUTES_RESET', true), - 'token' => (bool) env('PLAYGROUND_AUTH_ROUTES_TOKEN', true), - 'verify' => (bool) env('PLAYGROUND_AUTH_ROUTES_RESET', true), - ], - 'sitemap' => [ - 'enable' => (bool) env('PLAYGROUND_AUTH_SITEMAP_ENABLE', true), - 'guest' => (bool) env('PLAYGROUND_AUTH_SITEMAP_GUEST', true), - 'user' => (bool) env('PLAYGROUND_AUTH_SITEMAP_USER', true), - 'view' => (string) env('PLAYGROUND_AUTH_SITEMAP_VIEW', 'playground-auth::sitemap'), + 'translations' => (bool) env('PLAYGROUND_AUTH_LOAD_TRANSLATIONS', true), ], /** * Provide an array of email addresses for admin privileges. */ 'admins' => [ - // 'root@example.com', - // 'admin@example.com', + // 'cindy@example.com', + // 'joe@example.com', + // 'tim@example.com', ], /** * Provide an array of email addresses for manager privileges. */ 'managers' => [ - // 'manager@example.com', + // 'sally@example.com', ], - 'privileges' => [ + 'abilities' => [ 'root' => [ '*', - 'app:*', - 'playground:*', - 'playground-auth:*', - - 'playground-matrix:*', - 'playground-matrix-resource:*', - - 'playground-matrix-resource:backlog:*', - 'playground-matrix-resource:board:*', - 'playground-matrix-resource:epic:*', - 'playground-matrix-resource:flow:*', - 'playground-matrix-resource:milestone:*', - 'playground-matrix-resource:note:*', - 'playground-matrix-resource:project:*', - 'playground-matrix-resource:release:*', - 'playground-matrix-resource:roadmap:*', - 'playground-matrix-resource:source:*', - 'playground-matrix-resource:sprint:*', - 'playground-matrix-resource:tag:*', - 'playground-matrix-resource:team:*', - 'playground-matrix-resource:ticket:*', - 'playground-matrix-resource:version:*', ], 'admin' => [ 'app:*', @@ -80,22 +44,6 @@ 'playground-auth:*', 'playground-matrix:*', 'playground-matrix-resource:*', - - 'playground-matrix-resource:backlog:*', - 'playground-matrix-resource:board:*', - 'playground-matrix-resource:epic:*', - 'playground-matrix-resource:flow:*', - 'playground-matrix-resource:milestone:*', - 'playground-matrix-resource:note:*', - 'playground-matrix-resource:project:*', - 'playground-matrix-resource:release:*', - 'playground-matrix-resource:roadmap:*', - 'playground-matrix-resource:source:*', - 'playground-matrix-resource:sprint:*', - 'playground-matrix-resource:tag:*', - 'playground-matrix-resource:team:*', - 'playground-matrix-resource:ticket:*', - 'playground-matrix-resource:version:*', ], 'manager' => [ 'app:view', @@ -171,43 +119,46 @@ 'playground-matrix-resource:version:viewAny', ], 'guest' => [ - 'app:view', + 'none', + ], + // 'guest' => [ + // 'app:view', - 'playground:view', + // 'playground:view', - 'playground-auth:logout', - 'playground-auth:reset-password', + // 'playground-auth:logout', + // 'playground-auth:reset-password', - 'playground-matrix-resource:backlog:view', - 'playground-matrix-resource:backlog:viewAny', - 'playground-matrix-resource:board:view', - 'playground-matrix-resource:board:viewAny', - 'playground-matrix-resource:epic:view', - 'playground-matrix-resource:epic:viewAny', - 'playground-matrix-resource:flow:view', - 'playground-matrix-resource:flow:viewAny', - 'playground-matrix-resource:milestone:view', - 'playground-matrix-resource:milestone:viewAny', - 'playground-matrix-resource:note:view', - 'playground-matrix-resource:note:viewAny', - 'playground-matrix-resource:project:view', - 'playground-matrix-resource:project:viewAny', - 'playground-matrix-resource:release:view', - 'playground-matrix-resource:release:viewAny', - 'playground-matrix-resource:roadmap:view', - 'playground-matrix-resource:roadmap:viewAny', - 'playground-matrix-resource:source:view', - 'playground-matrix-resource:source:viewAny', - 'playground-matrix-resource:sprint:view', - 'playground-matrix-resource:sprint:viewAny', - 'playground-matrix-resource:tag:view', - 'playground-matrix-resource:tag:viewAny', - 'playground-matrix-resource:team:view', - 'playground-matrix-resource:team:viewAny', - 'playground-matrix-resource:ticket:view', - 'playground-matrix-resource:ticket:viewAny', - 'playground-matrix-resource:version:view', - 'playground-matrix-resource:version:viewAny', - ], + // 'playground-matrix-resource:backlog:view', + // 'playground-matrix-resource:backlog:viewAny', + // 'playground-matrix-resource:board:view', + // 'playground-matrix-resource:board:viewAny', + // 'playground-matrix-resource:epic:view', + // 'playground-matrix-resource:epic:viewAny', + // 'playground-matrix-resource:flow:view', + // 'playground-matrix-resource:flow:viewAny', + // 'playground-matrix-resource:milestone:view', + // 'playground-matrix-resource:milestone:viewAny', + // 'playground-matrix-resource:note:view', + // 'playground-matrix-resource:note:viewAny', + // 'playground-matrix-resource:project:view', + // 'playground-matrix-resource:project:viewAny', + // 'playground-matrix-resource:release:view', + // 'playground-matrix-resource:release:viewAny', + // 'playground-matrix-resource:roadmap:view', + // 'playground-matrix-resource:roadmap:viewAny', + // 'playground-matrix-resource:source:view', + // 'playground-matrix-resource:source:viewAny', + // 'playground-matrix-resource:sprint:view', + // 'playground-matrix-resource:sprint:viewAny', + // 'playground-matrix-resource:tag:view', + // 'playground-matrix-resource:tag:viewAny', + // 'playground-matrix-resource:team:view', + // 'playground-matrix-resource:team:viewAny', + // 'playground-matrix-resource:ticket:view', + // 'playground-matrix-resource:ticket:viewAny', + // 'playground-matrix-resource:version:view', + // 'playground-matrix-resource:version:viewAny', + // ], ], ]; diff --git a/database/migrations-laravel/2014_10_12_000000_create_users_table.php b/database/migrations-laravel/2014_10_12_000000_create_users_table.php index 6c2b6d5..1f97419 100644 --- a/database/migrations-laravel/2014_10_12_000000_create_users_table.php +++ b/database/migrations-laravel/2014_10_12_000000_create_users_table.php @@ -4,7 +4,8 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class () extends Migration { +return new class() extends Migration +{ /** * Run the migrations. */ diff --git a/database/migrations-laravel/2014_10_12_100000_create_password_reset_tokens_table.php b/database/migrations-laravel/2014_10_12_100000_create_password_reset_tokens_table.php index d8336e7..8b5b388 100644 --- a/database/migrations-laravel/2014_10_12_100000_create_password_reset_tokens_table.php +++ b/database/migrations-laravel/2014_10_12_100000_create_password_reset_tokens_table.php @@ -4,7 +4,8 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class () extends Migration { +return new class() extends Migration +{ /** * Run the migrations. */ diff --git a/database/migrations-laravel/2019_08_19_000000_create_failed_jobs_table.php b/database/migrations-laravel/2019_08_19_000000_create_failed_jobs_table.php deleted file mode 100644 index 667f82c..0000000 --- a/database/migrations-laravel/2019_08_19_000000_create_failed_jobs_table.php +++ /dev/null @@ -1,31 +0,0 @@ -id(); - $table->string('uuid')->unique(); - $table->text('connection'); - $table->text('queue'); - $table->longText('payload'); - $table->longText('exception'); - $table->timestamp('failed_at')->useCurrent(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('failed_jobs'); - } -}; diff --git a/database/migrations-laravel/2019_12_14_000001_create_personal_access_tokens_table.php b/database/migrations-laravel/2019_12_14_000001_create_personal_access_tokens_table.php index 668cd96..0fc7a63 100644 --- a/database/migrations-laravel/2019_12_14_000001_create_personal_access_tokens_table.php +++ b/database/migrations-laravel/2019_12_14_000001_create_personal_access_tokens_table.php @@ -4,7 +4,8 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class () extends Migration { +return new class() extends Migration +{ /** * Run the migrations. */ diff --git a/database/migrations-playground/2014_10_12_000000_create_users_table.php b/database/migrations-playground/2014_10_12_000000_create_users_table.php new file mode 100644 index 0000000..d23981e --- /dev/null +++ b/database/migrations-playground/2014_10_12_000000_create_users_table.php @@ -0,0 +1,121 @@ +uuid('id')->primary(); + + $table->uuid('created_id')->nullable()->index(); + $table->uuid('modified_id')->nullable()->index(); + + // Date columns + + $table->timestamps(); + $table->timestamp('email_verified_at')->nullable(); + + // Status + + $table->softDeletes(); + + $table->tinyInteger('active')->unsigned()->index()->default(0); + $table->tinyInteger('banned')->unsigned()->index()->default(0); + $table->tinyInteger('closed')->unsigned()->index()->default(0); + $table->tinyInteger('flagged')->unsigned()->index()->default(0); + $table->tinyInteger('internal')->unsigned()->index()->default(0); + $table->tinyInteger('locked')->unsigned()->index()->default(0); + + // UI + + $table->string('style', 128)->default(''); + $table->string('klass', 128)->default(''); + $table->string('icon', 128)->default(''); + + // Entity columns + + $table->string('name')->default(''); + $table->string('email')->unique(); + $table->string('password')->default(''); + $table->string('phone')->default(''); + $table->string('locale')->default(''); + $table->string('timezone')->default(''); + + $table->rememberToken(); + + $table->string('role')->default(''); + + // A link to the external source of the user. + $table->string('url', 512)->default(''); + + // Description is an internal field. + $table->string('description', 512)->default(''); + + $table->string('image', 512)->default(''); + $table->string('avatar', 512)->default(''); + + // The introduction should be the first 255 characters or less of the content. + // The introduction is visible to the client. No HTML. + $table->string('introduction', 512)->default(''); + + // The HTML content of the user. + $table->mediumText('content')->nullable(); + + // The summary of the content, HTML allowed, to be shown to the client. + $table->mediumText('summary')->nullable(); + + $table->json('abilities') + ->default(new Expression('(JSON_ARRAY())')) + ->comment('Array of ability strings'); + $table->json('accounts') + ->default(new Expression('(JSON_OBJECT())')) + ->comment('User accounts object'); + $table->json('address') + ->default(new Expression('(JSON_OBJECT())')) + ->comment('User address object'); + $table->json('contact') + ->default(new Expression('(JSON_OBJECT())')) + ->comment('User contact object'); + $table->json('meta') + ->default(new Expression('(JSON_OBJECT())')) + ->comment('Model meta object'); + $table->json('notes') + ->default(new Expression('(JSON_ARRAY())')) + ->comment('Array of note objects'); + $table->json('options') + ->default(new Expression('(JSON_OBJECT())')) + ->comment('Model options object'); + $table->json('registration') + ->default(new Expression('(JSON_OBJECT())')) + ->comment('Registration information object'); + $table->json('roles') + ->default(new Expression('(JSON_ARRAY())')) + ->comment('Array of role strings'); + $table->json('permissions') + ->default(new Expression('(JSON_ARRAY())')) + ->comment('Array of permission strings'); + $table->json('privileges') + ->default(new Expression('(JSON_ARRAY())')) + ->comment('Array of privilege strings'); + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + } +}; diff --git a/database/migrations-playground/2014_10_12_100000_create_password_reset_tokens_table.php b/database/migrations-playground/2014_10_12_100000_create_password_reset_tokens_table.php new file mode 100644 index 0000000..8b5b388 --- /dev/null +++ b/database/migrations-playground/2014_10_12_100000_create_password_reset_tokens_table.php @@ -0,0 +1,28 @@ +string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('password_reset_tokens'); + } +}; diff --git a/database/migrations-playground/2019_12_14_000001_create_personal_access_tokens_table.php b/database/migrations-playground/2019_12_14_000001_create_personal_access_tokens_table.php new file mode 100644 index 0000000..a297dc2 --- /dev/null +++ b/database/migrations-playground/2019_12_14_000001_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->uuidMorphs('tokenable'); + $table->string('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/lang/en/auth.php b/lang/en/auth.php index d4c7641..c97ab61 100644 --- a/lang/en/auth.php +++ b/lang/en/auth.php @@ -18,4 +18,5 @@ 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 'required' => 'Authentication is required. Please log in.', + 'sanctum.disabled' => 'Sorry, Sanctum authenticaction is currently disabled.', ]; diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..76ff8d2 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,22 @@ +includes: + - vendor/larastan/larastan/extension.neon + - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-phpunit/rules.neon + +parameters: + level: 9 + + paths: + - config + - database + - lang + - src + - tests + + excludePaths: + - 'tests/logs/*' + - 'output/*' + + treatPhpDocTypesAsCertain: false + + checkGenericClassInNonGenericObjectType: false diff --git a/resources/docs/artisan-about-playground-auth.png b/resources/docs/artisan-about-playground-auth.png index 8a5a8d5c00fe6e1be3402fc9832399dd710b92ba..89fad01e9b865317cc09197121e2f7ffaa6a0f59 100644 GIT binary patch literal 44527 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�qNHKg}| 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 diff --git a/resources/views/confirm-password.blade.php b/resources/views/confirm-password.blade.php deleted file mode 100644 index 44eea23..0000000 --- a/resources/views/confirm-password.blade.php +++ /dev/null @@ -1,95 +0,0 @@ -@extends($package_config_auth['layout'], [ - // 'withSidebarLeft' => false, - // 'withSidebarRight' => false, -]) -@section('title', __('Confirm Password')) - -@section('breadcrumbs') - -@endsection - -@section('content') -
-
-
-
-
{{ __('Confirm Password') }}
- -
-
- @csrf - -
- - -
- - - @error('email') - - {{ $message }} - - @enderror -
-
- -
- - -
- - - @error('password') - - {{ $message }} - - @enderror -
-
- -
-
- -
-
-
-
-
-
-
-
-@endsection diff --git a/resources/views/forgot-password.blade.php b/resources/views/forgot-password.blade.php deleted file mode 100644 index ad1b062..0000000 --- a/resources/views/forgot-password.blade.php +++ /dev/null @@ -1,95 +0,0 @@ -@extends($package_config_auth['layout'], [ - // 'withSidebarLeft' => false, - // 'withSidebarRight' => false, -]) -@section('title', __('Forgot Password')) - -@section('breadcrumbs') - -@endsection - -@section('content') -
-
-
-
-
{{ __('Forgot Password') }}
- -
-
- @csrf - -
- - -
- - - @error('email') - - {{ $message }} - - @enderror -
-
- -
- - -
- - - @error('password') - - {{ $message }} - - @enderror -
-
- -
-
- -
-
-
-
-
-
-
-
-@endsection diff --git a/resources/views/login.blade.php b/resources/views/login.blade.php deleted file mode 100644 index 5fdcc3f..0000000 --- a/resources/views/login.blade.php +++ /dev/null @@ -1,102 +0,0 @@ -@extends($package_config_auth['layout'], [ - // 'withSidebarLeft' => false, - // 'withSidebarRight' => false, -]) -@section('title', __('Login')) - -@section('breadcrumbs') - -@endsection - -@section('content') -
-
-
-
-
{{ __('Login') }}
- -
-
- @csrf - -
- - -
- - - @error('email') - - {{ $message }} - - @enderror -
-
- -
- - -
- - - @error('password') - - {{ $message }} - - @enderror -
-
- -
-
-
- - - -
-
-
- -
-
- - - @if (Route::has('password.request')) - - {{ __('Forgot Your Password?') }} - - @endif -
-
-
-
-
-
-
-
-@endsection diff --git a/resources/views/register.blade.php b/resources/views/register.blade.php deleted file mode 100644 index 69191b5..0000000 --- a/resources/views/register.blade.php +++ /dev/null @@ -1,101 +0,0 @@ -@extends($package_config_auth['layout'], [ - // 'withSidebarLeft' => false, - // 'withSidebarRight' => false, -]) -@section('title', __('Register')) - -@section('breadcrumbs') - -@endsection - -@section('content') -
-
-
-
-
{{ __('Register') }}
- -
-
- @csrf - -
- - -
- - - @error('email') - - {{ $message }} - - @enderror -
-
- -
- - -
- - - @error('password') - - {{ $message }} - - @enderror -
-
- -
-
- - - @if (Route::has('password.request')) - - {{ __('Forgot Your Password?') }} - - @endif -
-
-
-
-
-
-
-
-@endsection diff --git a/resources/views/reset-password.blade.php b/resources/views/reset-password.blade.php deleted file mode 100644 index f914611..0000000 --- a/resources/views/reset-password.blade.php +++ /dev/null @@ -1,93 +0,0 @@ -@extends($package_config_auth['layout'], [ - // 'withSidebarLeft' => false, - // 'withSidebarRight' => false, -]) -@section('title', __('Reset Password')) - -@section('breadcrumbs') - -@endsection - -@section('content') -
-
-
-
-
{{ __('Forgot Password') }}
- -
-
- @csrf - -
- - -
- - - @error('email') - - {{ $message }} - - @enderror -
-
- -
- - -
- - - @error('password') - - {{ $message }} - - @enderror -
-
- -
-
- -
-
-
-
-
-
-
-
-@endsection diff --git a/resources/views/sitemap.blade.php b/resources/views/sitemap.blade.php deleted file mode 100644 index c9ff333..0000000 --- a/resources/views/sitemap.blade.php +++ /dev/null @@ -1,49 +0,0 @@ -
-
- -

{{ __('Authentication') }}

- -
- -
-
-
- {{ __('Handling your account credentials') }} - - {{ __('authentication, registration and passwords') }} - -
- -
-
- -
- -
-
diff --git a/resources/views/verify-email.blade.php b/resources/views/verify-email.blade.php deleted file mode 100644 index 7d7cfad..0000000 --- a/resources/views/verify-email.blade.php +++ /dev/null @@ -1,75 +0,0 @@ -@extends($package_config_auth['layout'], [ - // 'withSidebarLeft' => false, - // 'withSidebarRight' => false, -]) -@section('title', __('Verify Email')) - -@section('breadcrumbs') - -@endsection - -@section('content') -
-
-
-
-
{{ __('Verify Email') }}
- -
-
- @csrf - -
- - -
- - - @error('email') - - {{ $message }} - - @enderror -
-
- -
-
- -
-
-
-
-
-
-
-
-@endsection diff --git a/routes/auth.php b/routes/auth.php deleted file mode 100644 index 78c81c5..0000000 --- a/routes/auth.php +++ /dev/null @@ -1,129 +0,0 @@ - [ - 'web', - ], - 'namespace' => '\GammaMatrix\Playground\Auth\Http\Controllers', - ], function () { - Route::post('/logout', [ - 'uses' => 'AuthenticatedSessionController@destroy', - ]); - - Route::get('/logout', [ - 'as' => 'logout', - 'uses' => 'AuthenticatedSessionController@destroy', - ]); - }); -} - -Route::group([ - 'middleware' => [ - 'web', - 'guest', - ], - 'namespace' => '\GammaMatrix\Playground\Auth\Http\Controllers', -], function () { - if (!empty(config('playground-auth.routes.token'))) { - Route::get('/token', [ - 'as' => 'token', - 'uses' => 'AuthenticatedSessionController@token', - ]); - } - - if (!empty(config('playground-auth.routes.login'))) { - Route::get('/login', [ - 'as' => 'login', - 'uses' => 'AuthenticatedSessionController@create', - ]); - - Route::post('/login', [ - 'as' => 'login.post', - 'uses' => 'AuthenticatedSessionController@store', - ]); - } - - if (!empty(config('playground-auth.routes.register'))) { - Route::get('/register', [ - 'as' => 'register', - 'uses' => 'RegisteredUserController@create', - ]); - - Route::post('/register', [ - 'as' => 'register.post', - 'uses' => 'RegisteredUserController@store', - ]); - } - - if (!empty(config('playground-auth.routes.forgot'))) { - Route::get('/forgot-password', [ - 'as' => 'password.request', - 'uses' => 'PasswordResetLinkController@create', - ]); - - Route::post('/forgot-password', [ - 'as' => 'password.email', - 'uses' => 'PasswordResetLinkController@store', - ]); - } - - if (!empty(config('playground-auth.routes.reset'))) { - Route::get('/reset-password/{token}', [ - 'as' => 'password.reset', - 'uses' => 'NewPasswordController@create', - ]); - - Route::post('/reset-password', [ - 'as' => 'password.update', - 'uses' => 'NewPasswordController@store', - ]); - } -}); - -Route::group([ - 'middleware' => [ - 'web', - 'auth', - ], - 'namespace' => '\GammaMatrix\Playground\Auth\Http\Controllers', -], function () { - if (!empty(config('playground-auth.routes.verify'))) { - Route::get('/verify-email', [ - 'as' => 'verification.notice', - 'uses' => 'EmailVerificationController@show', - ]); - - Route::get('/verify-email/{id}/{hash}', [ - 'as' => 'verification.verify', - 'uses' => 'EmailVerificationController@verify', - 'middleware' => ['signed', 'throttle:6,1'], - ]); - - Route::post('/verify-email', [ - 'as' => 'verification.send', - 'uses' => 'EmailVerificationController@send', - 'middleware' => ['throttle:6,1'], - ]); - } - - if (!empty(config('playground-auth.routes.confirm'))) { - Route::get('/confirm-password', [ - 'as' => 'password.confirm', - 'uses' => 'ConfirmablePasswordController@show', - ]); - - Route::post('/confirm-password', [ - 'uses' => 'ConfirmablePasswordController@store', - ]); - } -}); diff --git a/src/Console/Commands/HashPassword.php b/src/Console/Commands/HashPassword.php index 22f5491..5845a6d 100644 --- a/src/Console/Commands/HashPassword.php +++ b/src/Console/Commands/HashPassword.php @@ -1,17 +1,16 @@ json = $this->option('json'); - $password = Hash::make($this->argument('password')); - if (!$this->json) { + if ($this->hasArgument('password')) { + $password = $this->argument('password'); + } else { + $password = password('Please provide the password to hash:'); + } + + $hashed = $password && is_string($password) ? Hash::make($password) : ''; + + if (! $this->json && $hashed) { $this->line(PHP_EOL); - $this->comment($password); + $this->comment($hashed); } if ($this->json) { - $this->line(json_encode( - ['password' => $password], - $this->option('pretty') ? JSON_PRETTY_PRINT : null - )); + $output = json_encode( + ['hashed' => $hashed], + $this->option('pretty') ? JSON_PRETTY_PRINT : 0 + ); + if ($output) { + $this->line($output); + } } else { $this->line(PHP_EOL); } diff --git a/src/Http/Controllers/AuthenticatedSessionController.php b/src/Http/Controllers/AuthenticatedSessionController.php deleted file mode 100644 index 69d0e13..0000000 --- a/src/Http/Controllers/AuthenticatedSessionController.php +++ /dev/null @@ -1,246 +0,0 @@ - Route::has('password.request'), - 'status' => session('status'), - 'package_config' => $package_config, - 'package_config_auth' => $package_config_auth, - ]); - } - - /** - * - * TODO This should work with any kind of authentication system. Identify what is supported. - * - * Types: - * - User::$priviliges - * - User::hasPrivilige() - * - User::$roles - * - User::hasRole() - with string or array? - * - User::hasRoles() - * - Auth::user()?->currentAccessToken()?->can('app:*') - * - Auth::user()?->currentAccessToken()?->can($withPrivilege.':create') - * - * @experimental Subject to change - */ - protected function privileges(Authenticatable $user): array - { - $privileges = []; - - $hasRoles = !empty(config('playground-auth.token.roles')); - - $isAdmin = $hasRoles && $user->hasRole(['admin', 'wheel', 'root']); - $isManager = $hasRoles && $user->hasRole(['amanager']); - - $managers = config('playground-auth.managers'); - if (is_array($managers)) { - if ($user->email && in_array($user->email, $managers)) { - $isAdmin = false; - $isManager = true; - } - } - - $admins = config('playground-auth.admins'); - if (is_array($admins)) { - if ($user->email && in_array($user->email, $admins)) { - $isAdmin = true; - $isManager = false; - } - } - - if ($isAdmin) { - $privileges_admin = config('playground-auth.privileges.admin'); - if (is_array($privileges_admin)) { - foreach ($privileges_admin as $privilege) { - if (is_string($privilege) - && $privilege - && !in_array($privilege, $privileges) - ) { - $privileges[] = $privilege; - } - } - } - } elseif ($isManager) { - $privileges_manager = config('playground-auth.privileges.manager'); - if (is_array($privileges_manager)) { - foreach ($privileges_manager as $privilege) { - if (is_string($privilege) - && $privilege - && !in_array($privilege, $privileges) - ) { - $privileges[] = $privilege; - } - } - } - } else { - $privileges_user = config('playground-auth.privileges.user'); - if (is_array($privileges_user)) { - foreach ($privileges_user as $privilege) { - if (is_string($privilege) - && $privilege - && !in_array($privilege, $privileges) - ) { - $privileges[] = $privilege; - } - } - } - } - - return $privileges; - } - - /** - * - * NOTE: Creates multiple keys. Not sure if it is ok to reuse a token? - * TODO: This needs the device_name handling for Sanctum - */ - protected function issue(Request $request): array - { - $user = $request->user(); - - $tokens = []; - - $name = config('playground-auth.token.name'); - - $privileges = $this->privileges($user); - - $expiresAt = new Carbon(config('playground-auth.token.expires')); - - - - // use Laravel\Sanctum\PersonalAccessToken; - - // $token = PersonalAccessToken::findToken($hashedTooken); - - - // dd([ - // '__METHOD__' => __METHOD__, - // 'createToken' => $user->createToken($name, $privileges, $expiresAt)->toArray(), - // ]); - $tokens[$name] = $user->createToken($name, $privileges, $expiresAt)->plainTextToken; - - return $tokens; - } - - /** - * Authenticated the user. - * - * @route POST /login - */ - public function store(LoginRequest $request): JsonResponse|RedirectResponse - { - $request->authenticate(); - // dd([ - // '__METHOD__' => __METHOD__, - // // '$request' => $request, - // ]); - - $request->session()->regenerate(); - - $payload = [ - 'message' => __('authenticated'), - 'tokens' => [], - ]; - - if (!empty(config('playground-auth.token.sanctum'))) { - $payload['tokens'] = $this->issue($request); - } - - if (!empty(config('playground-auth.session'))) { - $payload['tokens']['session'] = $request->session()->token(); - } - - if ($request->expectsJson()) { - return response()->json($payload); - } - - return redirect()->intended($this->getRedirectUrl()); - } - - /** - * Destroy an authenticated session. - * - * @route GET /logout logout - * @route POST /logout - */ - public function destroy(Request $request): JsonResponse|RedirectResponse - { - $all = $request->has('all') || $request->has('everywhere'); - - if (!empty(config('playground-auth.token.sanctum'))) { - $user = $request->user(); - - if ($user) { - if ($all) { - $user->tokens()->delete(); - } else { - $token = $user->currentAccessToken(); - if ($token && is_callable($token, 'delete')) { - $token->delete(); - } - } - } - } - - Auth::guard('web')->logout(); - - $request->session()->invalidate(); - - $request->session()->regenerateToken(); - - if ($request->expectsJson()) { - return response()->json([ - 'message' => __('logout'), - 'session_token' => $request->session()->token(), - ]); - } - - return redirect('/'); - } - - /** - * Return a CSRF token. - * - */ - public function token(Request $request): JsonResponse - { - return response()->json([ - 'meta' => [ - 'token' => csrf_token(), - ] - ]); - } -} diff --git a/src/Http/Controllers/ConfirmablePasswordController.php b/src/Http/Controllers/ConfirmablePasswordController.php deleted file mode 100644 index dde980c..0000000 --- a/src/Http/Controllers/ConfirmablePasswordController.php +++ /dev/null @@ -1,59 +0,0 @@ - $package_config, - 'package_config_auth' => $package_config_auth, - ]); - } - - /** - * Confirm the user's password. - * - * @route POST /confirm-password - */ - public function store(Request $request): JsonResponse|RedirectResponse - { - if (!Auth::guard('web')->validate([ - 'email' => $request->user()->email, - 'password' => $request->password, - ])) { - throw ValidationException::withMessages([ - 'password' => __('auth.password'), - ]); - } - - $request->session()->put('auth.password_confirmed_at', time()); - - return redirect()->intended($this->getRedirectUrl()); - } -} diff --git a/src/Http/Controllers/Controller.php b/src/Http/Controllers/Controller.php deleted file mode 100644 index 896319f..0000000 --- a/src/Http/Controllers/Controller.php +++ /dev/null @@ -1,27 +0,0 @@ -user()->hasVerifiedEmail()) { - return redirect()->intended($this->getRedirectUrl()); - } - - $package_config = config('playground'); - $package_config_auth = config('playground-auth'); - - return view(sprintf('%1$s%2$s', $package_config_auth['view'], 'verify-email'), [ - 'package_config' => $package_config, - 'package_config_auth' => $package_config_auth, - ]); - } - - /** - * Send a new email verification notification. - * - * @route POST /verify-email verification.send - */ - public function send(Request $request): RedirectResponse|Response - { - if ($request->user()->hasVerifiedEmail()) { - return redirect()->intended($this->getRedirectUrl()); - } - - $request->user()->sendEmailVerificationNotification(); - - return back()->with('status', 'verification-link-sent'); - } - - /** - * Mark the authenticated user's email address as verified. - * - * @route POST /verify-email/{id}/{hash} verification.verify - */ - public function verify( - EmailVerificationRequest $request - ): Response|JsonResponse|RedirectResponse|View { - if ($request->user()->hasVerifiedEmail()) { - return redirect()->intended($this->getRedirectUrl().'?verified=1'); - } - - if ($request->user()->markEmailAsVerified()) { - event(new Verified($request->user())); - } - - return redirect()->intended($this->getRedirectUrl().'?verified=1'); - } -} diff --git a/src/Http/Controllers/NewPasswordController.php b/src/Http/Controllers/NewPasswordController.php deleted file mode 100644 index ca10001..0000000 --- a/src/Http/Controllers/NewPasswordController.php +++ /dev/null @@ -1,84 +0,0 @@ - $package_config, - 'package_config_auth' => $package_config_auth, - 'email' => $request->email, - 'token' => $request->route('token'), - ]); - } - - /** - * Handle an incoming new password request. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\RedirectResponse - * - * @throws \Illuminate\Validation\ValidationException - */ - public function store(Request $request) - { - $request->validate([ - 'token' => 'required', - 'email' => 'required|email', - 'password' => ['required', 'confirmed', Rules\Password::defaults()], - ]); - - // Here we will attempt to reset the user's password. If it is successful we - // will update the password on an actual user model and persist it to the - // database. Otherwise we will parse the error and return the response. - $status = Password::reset( - $request->only('email', 'password', 'password_confirmation', 'token'), - function ($user) use ($request) { - $user->forceFill([ - 'password' => Hash::make($request->password), - 'remember_token' => Str::random(60), - ])->save(); - - event(new PasswordReset($user)); - } - ); - - // If the password was successfully reset, we will redirect the user back to - // the application's home authenticated view. If there is an error we can - // redirect them back to where they came from with their error message. - if ($status == Password::PASSWORD_RESET) { - return redirect()->route('login')->with('status', __($status)); - } - - throw ValidationException::withMessages([ - 'email' => [trans($status)], - ]); - } -} diff --git a/src/Http/Controllers/PasswordResetLinkController.php b/src/Http/Controllers/PasswordResetLinkController.php deleted file mode 100644 index e669967..0000000 --- a/src/Http/Controllers/PasswordResetLinkController.php +++ /dev/null @@ -1,56 +0,0 @@ - $package_config, - 'package_config_auth' => $package_config_auth, - 'status' => session('status'), - ]); - } - - /** - * Handle an incoming password reset link request. - * - * @throws \Illuminate\Validation\ValidationException - */ - public function store(PasswordResetRequest $request): RedirectResponse - { - $validated = $request->validated(); - - $status = Password::sendResetLink($validated); - - if ($status == Password::RESET_LINK_SENT) { - return back()->with('status', __($status)); - } - - throw ValidationException::withMessages([ - 'email' => [trans($status)], - ]); - } -} diff --git a/src/Http/Controllers/RegisteredUserController.php b/src/Http/Controllers/RegisteredUserController.php deleted file mode 100644 index d045dd6..0000000 --- a/src/Http/Controllers/RegisteredUserController.php +++ /dev/null @@ -1,66 +0,0 @@ - $package_config, - 'package_config_auth' => $package_config_auth, - ]); - } - - /** - * Handle an incoming registration request. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\RedirectResponse - * - * @throws \Illuminate\Validation\ValidationException - */ - public function store(Request $request) - { - $request->validate([ - 'name' => 'required|string|max:255', - 'email' => 'required|string|email|max:255|unique:users', - 'password' => ['required', 'confirmed', Rules\Password::defaults()], - ]); - - $c = config('auth.providers.users.model', '\\App\\Models\\User'); - $user = $c::create([ - 'name' => $request->name, - 'email' => $request->email, - 'password' => Hash::make($request->password), - ]); - - // event(new Registered($user)); - - Auth::login($user); - - return redirect($this->getRedirectUrl()); - } -} diff --git a/src/Http/Requests/EmailVerificationRequest.php b/src/Http/Requests/EmailVerificationRequest.php deleted file mode 100644 index 32ec3ed..0000000 --- a/src/Http/Requests/EmailVerificationRequest.php +++ /dev/null @@ -1,38 +0,0 @@ -user(); - - if (!$user - || !hash_equals((string) $this->route('id'), (string) $user->id) - ) { - return false; - } - - if (!hash_equals((string) $this->route('hash'), sha1($user->email))) { - return false; - } - - return true; - } -} diff --git a/src/Http/Requests/LoginRequest.php b/src/Http/Requests/LoginRequest.php deleted file mode 100644 index 2aa1802..0000000 --- a/src/Http/Requests/LoginRequest.php +++ /dev/null @@ -1,101 +0,0 @@ -user()); - } - - /** - * Get the validation rules that apply to the request. - * - * @return array - */ - public function rules() - { - return [ - 'email' => ['required', 'string', 'email'], - 'password' => ['required', 'string'], - ]; - } - - /** - * Attempt to authenticate the request's credentials. - * - * @return void - * - * @throws \Illuminate\Validation\ValidationException - */ - public function authenticate() - { - $this->ensureIsNotRateLimited(); - - if (!Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { - RateLimiter::hit($this->throttleKey()); - - throw ValidationException::withMessages([ - 'email' => __('auth.failed'), - ]); - } - - RateLimiter::clear($this->throttleKey()); - } - - /** - * Ensure the login request is not rate limited. - * - * @return void - * - * @throws \Illuminate\Validation\ValidationException - */ - public function ensureIsNotRateLimited() - { - if (!RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { - return; - } - - event(new Lockout($this)); - - $seconds = RateLimiter::availableIn($this->throttleKey()); - - throw ValidationException::withMessages([ - 'email' => trans('auth.throttle', [ - 'seconds' => $seconds, - 'minutes' => ceil($seconds / 60), - ]), - ]); - } - - /** - * Get the rate limiting throttle key for the request. - * - * @return string - */ - public function throttleKey() - { - return Str::lower($this->input('email')).'|'.$this->ip(); - } -} diff --git a/src/Http/Requests/PasswordResetRequest.php b/src/Http/Requests/PasswordResetRequest.php deleted file mode 100644 index 23ad77f..0000000 --- a/src/Http/Requests/PasswordResetRequest.php +++ /dev/null @@ -1,38 +0,0 @@ -user()); - } - - /** - * Get the validation rules that apply to the request. - * - * @return array - */ - public function rules() - { - return [ - 'email' => ['required', 'email'], - ]; - } -} diff --git a/src/Issuer.php b/src/Issuer.php new file mode 100644 index 0000000..3719299 --- /dev/null +++ b/src/Issuer.php @@ -0,0 +1,233 @@ + + */ + protected array $abilities = []; + + protected bool $isRoot = false; + + protected bool $isAdmin = false; + + protected bool $isManager = false; + + protected bool $isUser = false; + + protected bool $isGuest = false; + + protected bool $hasAbilities = false; + + protected bool $hasPrivileges = false; + + protected bool $hasRoles = false; + + protected bool $hasSanctum = false; + + protected bool $useSanctum = false; + + protected bool $onlyUserAbilities = false; + + /** + * @return array + */ + protected function abilitiesByGroup(string $group): array + { + $abilities = config('playground-auth.abilities.'.$group); + + return is_array($abilities) ? $abilities : []; + } + + /** + * TODO This should work with any kind of authentication system. Identify what is supported. + * + * Types: + * - User::$priviliges + * - User::hasPrivilige() + * - User::$roles + * - User::hasRole() - with string or array? + * - User::hasRoles() + * - Auth::user()?->currentAccessToken()?->can('app:*') + * - Auth::user()?->currentAccessToken()?->can($withPrivilege.':create') + * + * @experimental Subject to change + * + * @return array + */ + protected function abilities(Authenticatable $user): array + { + $abilities = []; + + if ($this->onlyUserAbilities) { + $abilities = []; + } elseif ($this->isRoot) { + $abilities = $this->abilitiesByGroup('root'); + } elseif ($this->isAdmin) { + $abilities = $this->abilitiesByGroup('admin'); + } elseif ($this->isManager) { + $abilities = $this->abilitiesByGroup('manager'); + } elseif ($this->isUser) { + $abilities = $this->abilitiesByGroup('user'); + } elseif ($this->isGuest) { + $abilities = $this->abilitiesByGroup('guest'); + } + + foreach ($abilities as $ability) { + if (is_string($ability) + && $ability + && ! in_array($ability, $this->abilities) + ) { + $this->abilities[] = $ability; + } + } + + if (empty($this->abilities)) { + $this->abilities[] = 'none'; + } + + return $this->abilities; + } + + /** + * @param array $config + */ + public function init(Authenticatable $user, array $config): void + { + if ($user instanceof HasApiTokens) { + $this->hasSanctum = ! empty($config['sanctum']); + } else { + $this->hasSanctum = false; + } + + if ($user instanceof Abilities) { + if (empty($config['abilities'])) { + $this->abilities = []; + $this->onlyUserAbilities = false; + } else { + $abilities = $user->getAttributeValue('abilities'); + $this->abilities = is_array($abilities) ? $abilities : []; + $this->onlyUserAbilities = $config['abilities'] === 'user'; + } + } else { + $this->abilities = []; + $this->onlyUserAbilities = false; + } + + if ($user instanceof Admin) { + $this->isAdmin = $user->isAdmin(); + } else { + $this->isAdmin = false; + } + + if ($user instanceof Privileges) { + $this->hasPrivileges = ! empty($config['privileges']); + } else { + $this->hasPrivileges = false; + } + + if ($user instanceof Role) { + $this->hasRoles = ! empty($config['roles']); + if ($this->hasRoles) { + $this->isRoot = $user->hasRole('root'); + if (! $this->isAdmin) { + $this->isAdmin = $user->hasRole('admin'); + } + $this->isUser = $user->hasRole('user'); + $this->isManager = $user->hasRole('manager'); + if ($user->hasRole('guest')) { + $this->isGuest = true; + } + } + } else { + $this->hasRoles = false; + $this->isRoot = false; + $this->isUser = false; + $this->isGuest = false; + } + + if (! empty($config['listed'])) { + $this->listed($user); + } + + if (! $this->isGuest) { + $this->isGuest = ! ($this->isRoot || $this->isAdmin || $this->isManager || $this->isUser); + } + } + + public function listed(Authenticatable $user): void + { + $email = $user->getAttributeValue('email'); + $managers = config('playground-auth.managers'); + if (is_array($managers)) { + if ($email && in_array($email, $managers)) { + $this->isManager = true; + } + } + + $admins = config('playground-auth.admins'); + if (is_array($admins)) { + if ($email && in_array($email, $admins)) { + $this->isAdmin = true; + } + } + } + + /** + * @param Authenticatable&HasApiTokens $user + * @return array + */ + public function sanctum(HasApiTokens $user): array + { + /** + * @var array $config + */ + $config = config('playground-auth.token'); + + $this->init($user, $config); + + if (! $this->hasSanctum) { + throw new \Exception(__('playground-auth:auth.sanctum.disabled')); + } + + $tokens = []; + + $name = 'app'; + if (! empty($config['name']) && is_string($config['name'])) { + $name = $config['name']; + } + + // https://github.com/laravel/sanctum/pull/498 + $expiresAt = null; + if (! empty($config['expires']) && is_string($config['expires'])) { + $expiresAt = Carbon::parse($config['expires']); + } + + // dd([ + // '__METHOD__' => __METHOD__, + // 'createToken' => $user->createToken($name, $abilities, $expiresAt)->toArray(), + // ]); + $tokens[$name] = $user->createToken( + $name, + $this->abilities($user) + )->plainTextToken; + + return $tokens; + } +} diff --git a/src/Policies/Contracts/Role.php b/src/Policies/Contracts/Role.php new file mode 100644 index 0000000..b28af7f --- /dev/null +++ b/src/Policies/Contracts/Role.php @@ -0,0 +1,39 @@ + + */ + public function getRolesForAdmin(): array; + + /** + * Get the roles for standard actions. + * + * @return array + */ + public function getRolesForAction(): array; + + public function isRoot(Authenticatable $user): bool; + + /** + * Get the roles for view actions. + * + * @return array + */ + public function getRolesToView(): array; + + public function hasRole(Authenticatable $user, string $ability): bool|Response; +} diff --git a/src/Policies/ModelPolicy.php b/src/Policies/ModelPolicy.php new file mode 100644 index 0000000..5c53bfd --- /dev/null +++ b/src/Policies/ModelPolicy.php @@ -0,0 +1,129 @@ +verify($user, 'create'); + } + + /** + * Determine whether the user can delete the model. + * + * - This is for soft deletes or trash. + */ + public function delete( + Authenticatable $user, + Model $model + ): bool|Response { + // Models must be unlocked to allow deleting. + // NOTE: This lock check is bypassed by a root user. + if ($model->getAttribute('locked')) { + // return Response::denyWithStatus(423); + return Response::denyWithStatus(423, __('playground::auth.model.locked', [ + 'model' => Str::of(class_basename($model)) + ->snake()->replace('_', ' ')->title()->lower(), + ])); + } + + return $this->verify($user, 'delete'); + } + + /** + * Determine whether the user can view the model. + */ + public function detail(Authenticatable $user, Model $model): bool|Response + { + return $this->verify($user, 'view'); + } + + /** + * Determine whether the user can edit a model. + */ + public function edit(Authenticatable $user, Model $model = null): bool|Response + { + return $this->verify($user, 'edit'); + } + + /** + * Determine whether the user can permanently delete the model. + * + * Force deletes permanently from a database. + */ + public function forceDelete(Authenticatable $user, Model $model): bool|Response + { + return $this->verify($user, 'forceDelete'); + } + + /** + * Determine whether the user can lock a model. + */ + public function lock(Authenticatable $user, Model $model): bool|Response + { + return $this->verify($user, 'lock'); + } + + /** + * Determine whether the user can manage the model. + */ + public function manage(Authenticatable $user, Model $model): bool|Response + { + return $this->verify($user, 'manage'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(Authenticatable $user, Model $model): bool|Response + { + return $this->verify($user, 'restore'); + } + + /** + * Determine whether the user can store the model. + */ + public function store(Authenticatable $user): bool|Response + { + return $this->verify($user, 'store'); + } + + /** + * Determine whether the user can edit a model. + */ + public function update(Authenticatable $user, Model $model): bool|Response + { + // Models must be unlocked to allow updating. + // NOTE: This lock check is bypassed by a root user. + if ($model->getAttribute('locked')) { + // return Response::denyWithStatus(423); + return Response::denyWithStatus(423, __('playground::auth.model.locked', [ + 'model' => Str::of(class_basename($model))->snake()->replace('_', ' ')->title()->lower(), + ])); + } + + return $this->verify($user, 'update'); + } + + /** + * Determine whether the user can unlock a model. + */ + public function unlock(Authenticatable $user, Model $model): bool|Response + { + return $this->verify($user, 'unlock'); + } +} diff --git a/src/Policies/Policy.php b/src/Policies/Policy.php new file mode 100644 index 0000000..d64401a --- /dev/null +++ b/src/Policies/Policy.php @@ -0,0 +1,93 @@ +package)) { + $this->package = Str::of(__NAMESPACE__)->betweenFirst('\\', '\\')->slug()->toString(); + } + + if (empty($this->entity)) { + $this->entity = Str::of(class_basename(get_called_class()))->before('Policy')->slug()->toString(); + } + + // \Log::debug(__METHOD__, [ + // '$user' => $user, + // '$ability' => $ability, + // '$this->allowRootOverride' => $this->allowRootOverride, + // ]); + // dd([ + // '__METHOD__' => __METHOD__, + // '__FILE__' => __FILE__, + // '__LINE__' => __LINE__, + // 'static::class' => static::class, + // '$user' => $user ? $user->toArray(): $user, + // '$ability' => $ability, + // '$this->allowRootOverride' => $this->allowRootOverride, + // '$this->package' => $this->package, + // '$this->entity' => $this->entity, + // ]); + if ($this->allowRootOverride && $this->isRoot($user)) { + return true; + } + + return null; + } + + //////////////////////////////////////////////////////////////////////////// + // + // Abilities + // + //////////////////////////////////////////////////////////////////////////// + + /** + * Determine whether the user can view the index. + */ + public function index(Authenticatable $user): bool|Response + { + // \Log::debug(__METHOD__, [ + // '$user' => $user, + // ]); + return $this->verify($user, 'viewAny'); + } + + /** + * Determine whether the user can view. + */ + public function view(Authenticatable $user): bool|Response + { + // \Log::debug(__METHOD__, [ + // '$user' => $user, + // ]); + return $this->verify($user, 'view'); + } +} diff --git a/src/Policies/PolicyTrait.php b/src/Policies/PolicyTrait.php new file mode 100644 index 0000000..1584730 --- /dev/null +++ b/src/Policies/PolicyTrait.php @@ -0,0 +1,85 @@ +entity; + } + + public function getPackage(): string + { + return $this->package; + } + + public function hasToken(): bool + { + return ! empty($this->token); + } + + public function getToken(): ?PersonalAccessToken + { + return $this->token; + } + + public function setToken(PersonalAccessToken $token = null): self + { + $this->token = $token; + + return $this; + } + + public function verify(Authenticatable $user, string $ability): bool|Response + { + $verify = config('playground.auth.verify'); + // dd([ + // '__METHOD__' => __METHOD__, + // '$verify' => $verify, + // '$ability' => $ability, + // '$user' => $user, + // ]); + if ($verify === 'privileges') { + return $this->hasPrivilege($user, $this->privilege($ability)); + } elseif ($verify === 'roles') { + return $this->hasRole($user, $ability); + } elseif ($verify === 'user') { + // A user with an email address passes. + return ! empty($user->getAttribute('email')); + } + Log::debug(__METHOD__, [ + '$ability' => $ability, + '$user' => $user, + ]); + + return false; + } +} diff --git a/src/Policies/PrivilegeTrait.php b/src/Policies/PrivilegeTrait.php new file mode 100644 index 0000000..e2f40f4 --- /dev/null +++ b/src/Policies/PrivilegeTrait.php @@ -0,0 +1,139 @@ +getPackage())) { + $privilege .= $this->getPackage(); + } + + if (! empty($this->getEntity())) { + if (! empty($privilege)) { + $privilege .= ':'; + } + $privilege .= $this->getEntity(); + } + + if (! empty($ability)) { + if (! empty($privilege)) { + $privilege .= ':'; + } + $privilege .= $ability; + } + + return $privilege; + } + + private function hasPrivilegeWildcard(string $privilege): bool + { + $check = ''; + foreach (explode(':', $privilege) as $part) { + if ($check) { + $check .= ':'; + } + $check .= $part; + if ($this->getToken()?->can($check.':*')) { + return true; + } + } + + return false; + } + + public function hasPrivilege(Authenticatable $user, string $privilege): bool|Response + { + if (empty($privilege)) { + return Response::denyWithStatus(406, __('playground::auth.unacceptable')); + } + + if (config('playground.auth.sanctum')) { + + if ($user instanceof HasApiTokens) { + return $this->hasPrivilegeSanctum($user, $privilege); + } else { + return Response::denyWithStatus(401, __('playground::auth.unauthorized')); + } + } + + if (config('playground.auth.hasPrivilege') && method_exists($user, 'hasPrivilege')) { + return $user->hasPrivilege($privilege); + } + + if (config('playground.auth.userPrivileges') && array_key_exists('privileges', $user->getAttributes())) { + $privileges = $user->getAttribute('privileges'); + if (is_array($privileges) && in_array($privilege, $privileges)) { + return true; + } + } + + return Response::denyWithStatus(401, __('playground::auth.unauthorized')); + } + + private function hasPrivilegeSanctum(HasApiTokens $user, string $privilege): bool|Response + { + if (empty($privilege)) { + return Response::denyWithStatus(406, __('playground::auth.unacceptable')); + } + + if (! $this->hasToken()) { + /** + * @var PersonalAccessToken $token + */ + $token = $user->tokens() + ->where('name', config('playground.auth.token.name')) + // Get the latest created token. + ->orderBy('created_at', 'desc') + ->first(); + + if ($token) { + $this->setToken($token); + $user->withAccessToken($token); + } else { + return Response::denyWithStatus(401, __('playground::auth.unauthorized')); + } + } + + if ($this->hasPrivilegeWildcard($privilege)) { + return true; + } + + $token = $this->getToken(); + + if (! $token || $token->cant($privilege)) { + return Response::denyWithStatus(401, __('playground::auth.unauthorized')); + } + + // dd([ + // '__METHOD__' => __METHOD__, + // '$privilege' => $privilege, + // '$user->tokens()->first()' => $user->tokens()->where('name', config('playground-auth.token.name'))->first()->can($privilege), + // '$user->currentAccessToken()->can($privilege)' => $user->currentAccessToken()->can($privilege), + // '$user->currentAccessToken()->cant($privilege)' => $user->currentAccessToken()->cant($privilege), + // ]); + return true; + } +} diff --git a/src/Policies/RoleTrait.php b/src/Policies/RoleTrait.php new file mode 100644 index 0000000..a17814f --- /dev/null +++ b/src/Policies/RoleTrait.php @@ -0,0 +1,158 @@ + The roles allowed for actions in the MVC. + */ + protected $rolesForAction = [ + 'admin', + 'wheel', + 'root', + ]; + + /** + * @var array The roles allowed for admin actions in the MVC. + */ + protected $rolesForAdmin = [ + 'admin', + 'wheel', + 'root', + ]; + + /** + * @var array The roles allowed to view the MVC. + */ + protected $rolesToView = [ + 'admin', + 'wheel', + 'root', + ]; + + /** + * Get the roles for admin actions. + * + * @return array + */ + public function getRolesForAdmin(): array + { + return $this->rolesForAdmin; + } + + /** + * Get the roles for standard actions. + * + * @return array + */ + public function getRolesForAction(): array + { + return $this->rolesForAction; + } + + public function isRoot(Authenticatable $user): bool + { + $isRoot = false; + + if (! empty(config('playground.auth.userRole'))) { + $isRoot = $user->getAttributeValue('role') === 'root'; + } + + return $isRoot; + } + + /** + * Get the roles for view actions. + * + * @return array + */ + public function getRolesToView(): array + { + return $this->rolesToView; + } + + public function hasRole(Authenticatable $user, string $ability): bool|Response + { + if (in_array($ability, [ + 'show', + 'detail', + 'index', + 'view', + 'viewAny', + ])) { + $roles = $this->getRolesToView(); + } elseif (in_array($ability, [ + 'create', + 'edit', + 'manage', + 'store', + 'update', + ])) { + $roles = $this->getRolesForAction(); + } elseif (in_array($ability, [ + 'delete', + 'forceDelete', + 'lock', + 'unlock', + 'restore', + ])) { + $roles = $this->getRolesForAdmin(); + } else { + $roles = $this->getRolesForAdmin(); + // // Invalid role + // return Response::denyWithStatus(406, __('playground::auth.unacceptable')); + } + + if (config('playground.auth.hasRole') && method_exists($user, 'hasRole')) { + // Check for any role. + foreach ($roles as $role) { + if ($user->hasRole($role)) { + return true; + } + } + } + + if (config('playground.auth.userRole')) { + // Check for any role. + foreach ($roles as $role) { + if (! empty($user->role) && $role === $user->role) { + return true; + } + } + } + + if (config('playground.auth.userRoles')) { + if (is_array($roles) + && ! empty($user->roles) + && is_array($user->roles) + ) { + foreach ($roles as $role) { + if (in_array($role, $user->roles)) { + return true; + } + } + } + } + + // dd([ + // '__METHOD__' => __METHOD__, + // '__METHOD__' => __METHOD__, + // '$user' => $user, + // 'userRole' => config('playground.auth.userRole'), + // 'userRoles' => config('playground.auth.userRoles'), + // 'hasRole' => config('playground.auth.hasRole') && method_exists($user, 'hasRole'), + // '$ability' => $ability, + // '$roles' => $roles, + // ]); + return Response::denyWithStatus(401, __('playground::auth.unauthorized')); + } +} diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 1decbc8..079021d 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -1,23 +1,20 @@ $config + */ $config = config($this->package); - if (!empty($config)) { - $this->loadTranslationsFrom( - dirname(__DIR__).'/resources/lang', - 'playground-auth' - ); - - if (!empty($config['load']['routes'])) { - $this->loadRoutesFrom(dirname(__DIR__) . '/routes/auth.php'); - } + if (! empty($config['load']) && is_array($config['load'])) { - if (!empty($config['load']['views'])) { - $this->loadViewsFrom( - dirname(__DIR__).'/resources/views', - 'playground-auth' + if (! empty($config['load']['translations'])) { + $this->loadTranslationsFrom( + dirname(__DIR__).'/resources/lang', + $this->package ); } if ($this->app->runningInConsole()) { - if (!empty($config['load']['commands'])) { + if (! empty($config['load']['commands'])) { $this->commands([ Console\Commands\HashPassword::class, ]); @@ -55,15 +47,8 @@ public function boot() // Publish configuration $this->publishes([ - dirname(__DIR__).'/config/playground-auth.php' - => config_path('playground-auth.php') + sprintf('%1$s/config/%2$s.php', dirname(__DIR__), $this->package) => config_path(sprintf('%1$s.php', $this->package)), ], 'playground-config'); - - // Publish routes - $this->publishes([ - dirname(__DIR__).'/routes/auth.php' - => base_path('routes/playground-auth.php') - ], 'playground-routes'); } } @@ -72,49 +57,109 @@ public function boot() /** * Register any application services. - * - * @return void */ - public function register() + public function register(): void { $this->mergeConfigFrom( - dirname(__DIR__) . '/config/playground-auth.php', - 'playground-auth' + sprintf('%1$s/config/%2$s.php', dirname(__DIR__), $this->package), + $this->package ); } - public function about() + public function about(): void { $config = config($this->package); + $config = is_array($config) ? $config : []; - $version = $this->version(); + $load = ! empty($config['load']) && is_array($config['load']) ? $config['load'] : []; + $token = ! empty($config['token']) && is_array($config['token']) ? $config['token'] : []; - AboutCommand::add('Playground Auth', fn () => [ - 'Load Commands' => !empty($config['load']['commands']) ? 'ENABLED' : 'DISABLED', - 'Load Routes' => !empty($config['load']['routes']) ? 'ENABLED' : 'DISABLED', - 'Load Views' => !empty($config['load']['views']) ? 'ENABLED' : 'DISABLED', + $listed_admins = 0; + if (! empty($config['admins']) && is_array($config['admins'])) { + $listed_admins = count($config['admins']); + } - 'Redirect' => empty($config['redirect']) ? '' : $config['redirect'], - 'View [layout]' => sprintf('[%s]', $config['layout']), - 'View [prefix]' => sprintf('[%s]', $config['view']), + $listed_managers = 0; + if (! empty($config['managers']) && is_array($config['managers'])) { + $listed_managers = count($config['managers']); + } - 'Sitemap Views' => !empty($config['sitemap']['enable']) ? 'ENABLED' : 'DISABLED', - 'Sitemap Guest' => !empty($config['sitemap']['guest']) ? 'ENABLED' : 'DISABLED', - 'Sitemap User' => !empty($config['sitemap']['user']) ? 'ENABLED' : 'DISABLED', - 'Sitemap [view]' => sprintf('[%s]', $config['sitemap']['view']), + $abilties_root = 0; + $abilties_root_one = ''; + $abilties_admin = 0; + $abilties_admin_one = ''; + $abilties_manager = 0; + $abilties_manager_one = ''; + $abilties_user = 0; + $abilties_user_one = ''; + $abilties_guest = 0; + $abilties_guest_one = ''; + + if (! empty($config['abilities']) && is_array($config['abilities'])) { + // Check abilities: root + if (! empty($config['abilities']['root']) && is_array($config['abilities']['root'])) { + $abilties_root = count($config['abilities']['root']); + if ($abilties_root === 1) { + $abilties_root_one = json_encode($config['abilities']['root']); + } + } + // Check abilities: admin + if (! empty($config['abilities']['admin']) && is_array($config['abilities']['admin'])) { + $abilties_admin = count($config['abilities']['admin']); + if ($abilties_admin === 1) { + $abilties_admin_one = json_encode($config['abilities']['admin']); + } + } + // Check abilities: manager + if (! empty($config['abilities']['manager']) && is_array($config['abilities']['manager'])) { + $abilties_manager = count($config['abilities']['manager']); + if ($abilties_manager === 1) { + $abilties_manager_one = json_encode($config['abilities']['manager']); + } + } + // Check abilities: user + if (! empty($config['abilities']['user']) && is_array($config['abilities']['user'])) { + $abilties_user = count($config['abilities']['user']); + if ($abilties_user === 1) { + $abilties_user_one = json_encode($config['abilities']['user']); + } + } + // Check abilities: guest + if (! empty($config['abilities']['guest']) && is_array($config['abilities']['guest'])) { + $abilties_guest = count($config['abilities']['guest']); + if ($abilties_guest === 1) { + $abilties_guest_one = json_encode($config['abilities']['guest']); + } + } + } + + $version = $this->version(); - 'Token [Expires]' => sprintf('[%s]', $config['token']['expires']), - 'Token Name' => $config['token']['name'], - 'Token Roles' => !empty($config['token']['roles']) ? 'ENABLED' : 'DISABLED', - 'Token Privileges' => !empty($config['token']['privileges']) ? 'ENABLED' : 'DISABLED', - 'Token Sanctum' => !empty($config['token']['sanctum']) ? 'ENABLED' : 'DISABLED', + AboutCommand::add('Playground: Auth', fn () => [ + 'Load Commands' => ! empty($load['commands']) ? 'ENABLED' : 'DISABLED', + 'Load Translations' => ! empty($load['translations']) ? 'ENABLED' : 'DISABLED', + + 'Token [Abilities]' => sprintf('[%s]', $token['abilities']), + 'Token [Expires]' => sprintf('[%s]', $token['expires']), + 'Token [Name]' => sprintf('[%s]', $token['name']), + 'Token Listed Admins' => ! empty($token['listed']) ? sprintf('%1$d', $listed_admins) : 'DISABLED', + 'Token Listed Managers' => ! empty($token['listed']) ? sprintf('%1$d', $listed_managers) : 'DISABLED', + 'Token Roles' => ! empty($token['roles']) ? 'ENABLED' : 'DISABLED', + 'Token Privileges' => ! empty($token['privileges']) ? 'ENABLED' : 'DISABLED', + 'Token Sanctum' => ! empty($token['sanctum']) ? 'ENABLED' : 'DISABLED', + + 'Abilities [admin]' => ! empty($abilties_admin) && empty($abilties_admin_one) ? sprintf('%1$d', $abilties_admin) : sprintf('%1$s', $abilties_admin_one), + 'Abilities [guest]' => ! empty($abilties_guest) && empty($abilties_guest_one) ? sprintf('%1$d', $abilties_guest) : sprintf('%1$s', $abilties_guest_one), + 'Abilities [manager]' => ! empty($abilties_manager) && empty($abilties_manager_one) ? sprintf('%1$d', $abilties_manager) : sprintf('%1$s', $abilties_manager_one), + 'Abilities [root]' => ! empty($abilties_root) && empty($abilties_root_one) ? sprintf('%1$d', $abilties_root) : sprintf('%1$s', $abilties_root_one), + 'Abilities [user]' => ! empty($abilties_user) && empty($abilties_user_one) ? sprintf('%1$d', $abilties_user) : sprintf('%1$s', $abilties_user_one), 'Package' => $this->package, 'Version' => $version, ]); } - public function version() + public function version(): string { return static::VERSION; } diff --git a/tests/Feature/Http/Controllers/AuthenticationRouteTest.php b/tests/Feature/Http/Controllers/AuthenticationRouteTest.php deleted file mode 100644 index 9fdd6f9..0000000 --- a/tests/Feature/Http/Controllers/AuthenticationRouteTest.php +++ /dev/null @@ -1,156 +0,0 @@ -get('/login'); - - $response->assertStatus(200); - } - - public function test_users_can_authenticate_using_the_login_screen() - { - $user = User::factory()->create(); - - $response = $this->post('/login', [ - 'email' => $user->email, - 'password' => 'password', - ]); - - $response->assertStatus(302); - - $this->assertAuthenticated(); - $response->assertRedirect('/'); - } - - public function test_users_cannot_authenticate_with_invalid_password() - { - $user = User::factory()->create(); - - $response = $this->post('/login', [ - 'email' => $user->email, - 'password' => 'wrong-password', - ]); - - $response->assertStatus(302); - - $this->assertGuest(); - } - - public function test_users_can_logout_on_get_request() - { - $user = User::factory()->create(); - // dd([ - // 'playground' => config('playground'), - // 'playground-auth' => config('playground-auth'), - // ]); - $response = $this->actingAs($user)->get('/logout'); - - $response->assertStatus(302); - - $this->assertGuest(); - } - - public function test_users_can_logout_on_post_request() - { - $user = User::factory()->create(); - - $response = $this->actingAs($user)->post('/logout'); - - $response->assertStatus(302); - - $this->assertGuest(); - } - - public function test_guests_can_logout_on_get_request() - { - $response = $this->get('/logout'); - - $response->assertStatus(302); - - $this->assertGuest(); - } - - public function tesst_guests_can_logout_on_post_request() - { - $response = $this->get('/logout'); - - $response->assertStatus(302); - - $this->assertGuest(); - } - - public function test_csfr_token_request() - { - $response = $this->get('/token'); - - $response->assertStatus(200); - - $response->assertJsonStructure([ - 'meta' => [ - 'token', - ], - ]); - } - - public function test_login_repeat_under_rate_limit_and_clear() - { - $user = User::factory()->create(); - - $limit = 2; - - for ($i = 0; $i < $limit; $i++) { - $response = $this->post('/login', [ - 'email' => $user->email, - 'password' => 'wrong-password', - ]); - $response->assertStatus(302); - } - - $response = $this->post('/login', [ - 'email' => $user->email, - 'password' => 'password', - ]); - - $response->assertStatus(302); - - $this->assertAuthenticated(); - $response->assertRedirect('/'); - } - - public function test_login_repeat_and_hit_rate_limit() - { - $user = User::factory()->create(); - - $limit = 6; - - for ($i = 0; $i < $limit; $i++) { - $response = $this->post('/login', [ - 'email' => $user->email, - 'password' => 'wrong-password', - ]); - $response->assertStatus(302); - } - - // 'Too many login attempts. Please try again in 59 seconds.' - $response->assertSessionHasErrors([ - 'email', - ]); - // $response->dump(); - // $response->dumpHeaders(); - // $response->dumpSession(); - } -} diff --git a/tests/Feature/Http/Controllers/EmailVerificationRouteTest.php b/tests/Feature/Http/Controllers/EmailVerificationRouteTest.php deleted file mode 100644 index f7bd60d..0000000 --- a/tests/Feature/Http/Controllers/EmailVerificationRouteTest.php +++ /dev/null @@ -1,208 +0,0 @@ -create([ - 'email_verified_at' => null, - ]); - - $response = $this->actingAs($user)->get('/verify-email'); - - $response->assertStatus(200); - } - - public function test_email_verification_screen_is_not_rendered_if_already_verified() - { - $user = User::factory()->create([ - 'email_verified_at' => now(), - ]); - - $response = $this->actingAs($user)->get('/verify-email'); - - $response->assertStatus(302); - } - - public function test_json_send_email_verification_notification_as_guest() - { - Notification::fake(); - - $response = $this->json('post', '/verify-email'); - $response->assertStatus(401); - - // $response->dump(); - - $response->assertJsonStructure([ - 'message', - ]); - - $response->assertExactJson([ - 'message' => 'Unauthenticated.', - ]); - - Notification::assertNothingSent(); - } - - public function test_send_email_verification_notification_as_guest_and_redirect() - { - Notification::fake(); - - $response = $this->post('/verify-email'); - $response->assertStatus(302); - $response->assertredirect('/login'); - - // $response->dump(); - - Notification::assertNothingSent(); - } - - public function test_send_email_verification_notification_as_user() - { - Notification::fake(); - $user = User::factory()->create([ - 'email_verified_at' => null, - ]); - - $response = $this->actingAs($user)->post('/verify-email'); - - // $response->dump(); - - $response->assertStatus(302); - - $response->assertSessionHas('status', 'verification-link-sent'); - - // Notification::assertNothingSent(); - Notification::assertSentTo( - [$user], - VerifyEmail::class - ); - } - - /** - * EmailVerificationController::send(). - * - * @return void - */ - public function test_send_email_verification_notification_when_already_verified() - { - Notification::fake(); - $user = User::factory()->create([ - 'email_verified_at' => now(), - ]); - - $response = $this->actingAs($user)->post('/verify-email'); - - // $response->dump(); - - $response->assertStatus(302); - - Notification::assertNothingSent(); - } - - public function test_email_can_be_verified() - { - $user = User::factory()->create([ - 'email_verified_at' => null, - ]); - - Event::fake(); - - $verificationUrl = URL::temporarySignedRoute( - 'verification.verify', - Carbon::now()->addMinutes(config('auth.verification.expire', 60)), - [ - 'id' => $user->id, - 'hash' => sha1($user->email), - ] - ); - - $response = $this->actingAs($user)->get($verificationUrl); - - Event::assertDispatched(Verified::class); - $this->assertTrue($user->fresh()->hasVerifiedEmail()); - $response->assertRedirect('/?verified=1'); - } - - public function test_email_is_not_verified_with_invalid_hash() - { - $user = User::factory()->create([ - 'email_verified_at' => null, - ]); - - $verificationUrl = URL::temporarySignedRoute( - 'verification.verify', - Carbon::now()->addMinutes(config('auth.verification.expire', 60)), - [ - 'id' => $user->id, - 'hash' => sha1($user->email . 'make-this-invalid'), - ] - ); - - $this->actingAs($user)->get($verificationUrl); - - $this->assertFalse($user->fresh()->hasVerifiedEmail()); - } - - public function test_email_is_not_verified_with_invalid_user_id() - { - $user = User::factory()->create([ - 'email_verified_at' => null, - ]); - - $verificationUrl = URL::temporarySignedRoute( - 'verification.verify', - Carbon::now()->addMinutes(config('auth.verification.expire', 60)), - [ - 'id' => $user->id . 'make-this-invalid', - 'hash' => sha1($user->email), - ] - ); - - $this->actingAs($user)->get($verificationUrl); - - $this->assertFalse($user->fresh()->hasVerifiedEmail()); - } - - public function test_email_verified_with_already_verified() - { - $user = User::factory()->create([ - 'email_verified_at' => now(), - ]); - - Event::fake(); - - $verificationUrl = URL::temporarySignedRoute( - 'verification.verify', - Carbon::now()->addMinutes(config('auth.verification.expire', 60)), - [ - 'id' => $user->id, - 'hash' => sha1($user->email), - ] - ); - - $response = $this->actingAs($user)->get($verificationUrl); - - Event::assertNotDispatched(Verified::class); - $this->assertTrue($user->fresh()->hasVerifiedEmail()); - $response->assertRedirect('/?verified=1'); - } -} diff --git a/tests/Feature/Http/Controllers/PasswordConfirmationRouteTest.php b/tests/Feature/Http/Controllers/PasswordConfirmationRouteTest.php deleted file mode 100644 index 37702f2..0000000 --- a/tests/Feature/Http/Controllers/PasswordConfirmationRouteTest.php +++ /dev/null @@ -1,48 +0,0 @@ -create(); - - $response = $this->actingAs($user)->get('/confirm-password'); - - $response->assertStatus(200); - } - - public function test_password_can_be_confirmed() - { - $user = User::factory()->create(); - - $response = $this->actingAs($user)->post('/confirm-password', [ - 'password' => 'password', - ]); - - $response->assertRedirect(); - $response->assertSessionHasNoErrors(); - } - - public function test_password_is_not_confirmed_with_invalid_password() - { - $user = User::factory()->create(); - - $response = $this->actingAs($user)->post('/confirm-password', [ - 'password' => 'wrong-password', - ]); - - $response->assertSessionHasErrors(); - } -} diff --git a/tests/Feature/Http/Controllers/PasswordResetRouteTest.php b/tests/Feature/Http/Controllers/PasswordResetRouteTest.php deleted file mode 100644 index 83895f6..0000000 --- a/tests/Feature/Http/Controllers/PasswordResetRouteTest.php +++ /dev/null @@ -1,119 +0,0 @@ -get('/forgot-password'); - - $response->assertStatus(200); - } - - public function test_reset_password_link_can_be_requested() - { - Notification::fake(); - - $user = User::factory()->create(); - - $this->post('/forgot-password', ['email' => $user->email]); - - Notification::assertSentTo($user, ResetPassword::class); - } - - public function test_reset_password_screen_can_be_rendered() - { - Notification::fake(); - - $user = User::factory()->create(); - - $this->post('/forgot-password', ['email' => $user->email]); - - Notification::assertSentTo($user, ResetPassword::class, function ($notification) { - $response = $this->get('/reset-password/'.$notification->token); - - $response->assertStatus(200); - - return true; - }); - } - - public function test_password_can_be_reset_with_valid_token() - { - Notification::fake(); - - $user = User::factory()->create(); - - $this->post('/forgot-password', ['email' => $user->email]); - - Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { - $response = $this->post('/reset-password', [ - 'token' => $notification->token, - 'email' => $user->email, - 'password' => 'password', - 'password_confirmation' => 'password', - ]); - - $response->assertSessionHasNoErrors(); - - return true; - }); - } - - public function test_password_reset_with_invalid_token() - { - Notification::fake(); - - $user = User::factory()->create(); - - $this->post('/forgot-password', ['email' => $user->email]); - - Notification::assertNotSentTo($user, ResetPassword::class, function ($notification) use ($user) { - $response = $this->post('/reset-password', [ - 'token' => 'not the valid token', - 'email' => $user->email, - 'password' => 'password', - 'password_confirmation' => 'password', - ]); - - $response->assertSessionHasErrors(); - - return false; - }); - } - - public function test_password_reset_with_invalid_user() - { - Notification::fake(); - - $user = User::factory()->make(); - - $this->post('/forgot-password', ['email' => $user->email]); - - Notification::assertNotSentTo($user, ResetPassword::class, function ($notification) use ($user) { - $response = $this->post('/reset-password', [ - 'token' => 'not the valid token', - 'email' => $user->email, - 'password' => 'password', - 'password_confirmation' => 'password', - ]); - - $response->assertSessionHasErrors(); - - return false; - }); - } -} diff --git a/tests/Feature/Http/Controllers/RegistrationRouteTest.php b/tests/Feature/Http/Controllers/RegistrationRouteTest.php deleted file mode 100644 index 14a5c72..0000000 --- a/tests/Feature/Http/Controllers/RegistrationRouteTest.php +++ /dev/null @@ -1,36 +0,0 @@ -get('/register'); - - $response->assertStatus(200); - } - - public function test_new_users_can_register() - { - $response = $this->post('/register', [ - 'name' => 'Test User', - 'email' => $this->faker()->email, - 'password' => 'password', - 'password_confirmation' => 'password', - ]); - - $this->assertAuthenticated(); - $response->assertRedirect('/'); - } -} diff --git a/tests/Feature/TestCase.php b/tests/Feature/TestCase.php index 4c29e93..058ed16 100644 --- a/tests/Feature/TestCase.php +++ b/tests/Feature/TestCase.php @@ -1,32 +1,22 @@ loadLaravelMigrations(); - $this->loadMigrationsFrom(dirname(dirname(__DIR__)) . '/database/migrations-laravel'); + if (! empty(env('TEST_DB_MIGRATIONS'))) { + if ($this->load_migrations_laravel) { + $this->loadMigrationsFrom(dirname(dirname(__DIR__)).'/database/migrations-laravel'); + } + if ($this->load_migrations_playground) { + $this->loadMigrationsFrom(dirname(dirname(__DIR__)).'/database/migrations-playground'); + } } } - - /** - * Set up the environment. - * - * @param \Illuminate\Foundation\Application $app - */ - protected function getEnvironmentSetUp($app) - { - // dd(__METHOD__); - $app['config']->set('auth.providers.users.model', 'GammaMatrix\\Playground\\Test\\Models\\User'); - $app['config']->set('playground.auth.verify', 'user'); - - $app['config']->set('playground-auth.redirect', true); - $app['config']->set('playground-auth.session', true); - - $app['config']->set('playground-auth.token.roles', false); - $app['config']->set('playground-auth.token.privileges', false); - $app['config']->set('playground-auth.token.name', 'app-testing'); - $app['config']->set('playground-auth.token.sanctum', false); - - $app['config']->set('playground-auth.load.commands', true); - $app['config']->set('playground-auth.load.routes', true); - $app['config']->set('playground-auth.load.views', true); - - $app['config']->set('playground-auth.routes.confirm', true); - $app['config']->set('playground-auth.routes.forgot', true); - $app['config']->set('playground-auth.routes.logout', true); - $app['config']->set('playground-auth.routes.login', true); - $app['config']->set('playground-auth.routes.register', true); - $app['config']->set('playground-auth.routes.reset', true); - $app['config']->set('playground-auth.routes.token', true); - $app['config']->set('playground-auth.routes.verify', true); - - $app['config']->set('playground-auth.sitemap.enable', true); - $app['config']->set('playground-auth.sitemap.guest', true); - $app['config']->set('playground-auth.sitemap.user', true); - - $app['config']->set('playground-auth.admins', []); - $app['config']->set('playground-auth.managers', []); - } } diff --git a/tests/Unit/Console/Commands/HashPassword/CommandTest.php b/tests/Unit/Console/Commands/HashPassword/CommandTest.php index 381665a..131b2eb 100644 --- a/tests/Unit/Console/Commands/HashPassword/CommandTest.php +++ b/tests/Unit/Console/Commands/HashPassword/CommandTest.php @@ -1,40 +1,50 @@ artisan('auth:hash-password --json "my-password"') - ->assertExitCode(0) - ->expectsOutputToContain('password') - ; + // $result = $this->withoutMockingConsoleOutput()->artisan('auth:hash-password --json "my-password"'); + // dump(Artisan::output()); + /** + * @var \Illuminate\Testing\PendingCommand $result + */ + $result = $this->artisan('auth:hash-password --json "my-password"'); + $result->assertExitCode(0); + $result->expectsOutputToContain('hashed'); } - public function test_command_auth_hash_password() + public function test_command_auth_hash_password(): void { - $this->artisan('auth:hash-password some-passord') - ->assertExitCode(0) - ; + /** + * @var \Illuminate\Testing\PendingCommand $result + */ + $result = $this->artisan('auth:hash-password some-passord'); + $result->assertExitCode(0); } - public function test_command_auth_hash_password_without_argument_and_fail() + public function test_command_auth_hash_password_without_argument_and_fail(): void { - $this->expectException(\Symfony\Component\Console\Exception\RuntimeException::class); - $this->expectExceptionMessage('Not enough arguments (missing: "password").'); + // $result = $this->withoutMockingConsoleOutput()->artisan('auth:hash-password --no-interaction'); + // dump(Artisan::output()); + // $this->expectException(\Symfony\Component\Console\Exception\RuntimeException::class); + // $this->expectExceptionMessage('Not enough arguments (missing: "password").'); - $this->artisan('auth:hash-password') - ->assertExitCode(1) - ; + /** + * @var \Illuminate\Testing\PendingCommand $result + */ + $result = $this->artisan('auth:hash-password --no-interaction'); + $result->assertExitCode(0); + $result->doesntExpectOutputToContain('hashed'); } } diff --git a/tests/Unit/Policies/ModelPolicy/AbstractRoleTest.php b/tests/Unit/Policies/ModelPolicy/AbstractRoleTest.php new file mode 100644 index 0000000..bb3531f --- /dev/null +++ b/tests/Unit/Policies/ModelPolicy/AbstractRoleTest.php @@ -0,0 +1,490 @@ + 'roles', + 'playground.auth.hasRole' => true, + 'playground.auth.userRoles' => true, + ]); + } + + public function test_create_without_role(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $this->assertInstanceOf(Response::class, $instance->create($user)); + } + + public function test_create_with_admin(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $role = 'admin'; + $roles = [ + 'root', + ]; + + $user->setAttribute('role', $role); + + $this->assertTrue($instance->create($user)); + } + + public function test_delete_without_role(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $this->assertInstanceOf(Response::class, $instance->delete($user, $model)); + } + + public function test_delete_with_admin(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $role = 'admin'; + $roles = [ + 'root', + ]; + + $user->setAttribute('role', $role); + $user->setAttribute('roles', $roles); + + $this->assertTrue($instance->delete($user, $model)); + } + + public function test_delete_locked_with_admin(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $model->setAttribute('locked', true); + + $role = 'admin'; + $roles = [ + 'root', + ]; + + $user->setAttribute('role', $role); + $user->setAttribute('roles', $roles); + + $response = $instance->delete($user, $model); + + $this->assertInstanceOf( + Response::class, + $response + ); + + $this->assertFalse($response->allowed()); + $this->assertTrue($response->denied()); + $this->assertSame(423, $response->status()); + + // These could be customized: + // $this->assertNull($response->code()); + // $this->assertNull($response->message()); + } + + public function test_detail_without_role(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $this->assertInstanceOf(Response::class, $instance->detail($user, $model)); + } + + public function test_detail_with_admin(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $role = 'admin'; + $roles = [ + 'root', + ]; + + $user->setAttribute('role', $role); + $user->setAttribute('roles', $roles); + + $this->assertTrue($instance->detail($user, $model)); + } + + public function test_edit_without_role(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $this->assertInstanceOf(Response::class, $instance->edit($user, $model)); + } + + public function test_edit_with_admin(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $role = 'admin'; + $roles = [ + 'root', + ]; + + $user->setAttribute('role', $role); + $user->setAttribute('roles', $roles); + + $this->assertTrue($instance->edit($user, $model)); + } + + public function test_forceDelete_without_role(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $this->assertInstanceOf(Response::class, $instance->forceDelete($user, $model)); + } + + public function test_forceDelete_with_admin(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $role = 'admin'; + $roles = [ + 'root', + ]; + + $user->setAttribute('role', $role); + $user->setAttribute('roles', $roles); + + $this->assertTrue($instance->forceDelete($user, $model)); + } + + public function test_lock_without_role(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $this->assertInstanceOf(Response::class, $instance->lock($user, $model)); + } + + public function test_lock_with_admin(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $role = 'admin'; + $roles = [ + 'root', + ]; + + $user->setAttribute('role', $role); + $user->setAttribute('roles', $roles); + + $this->assertTrue($instance->lock($user, $model)); + } + + public function test_manage_without_role(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $this->assertInstanceOf(Response::class, $instance->manage($user, $model)); + } + + public function test_manage_with_admin(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $role = 'admin'; + $roles = [ + 'root', + ]; + + $user->setAttribute('role', $role); + $user->setAttribute('roles', $roles); + + $this->assertTrue($instance->manage($user, $model)); + } + + public function test_restore_without_role(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $this->assertInstanceOf(Response::class, $instance->restore($user, $model)); + } + + public function test_restore_with_admin(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $role = 'admin'; + $roles = [ + 'root', + ]; + + $user->setAttribute('role', $role); + $user->setAttribute('roles', $roles); + + $this->assertTrue($instance->restore($user, $model)); + } + + public function test_store_without_role(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $this->assertInstanceOf(Response::class, $instance->store($user)); + } + + public function test_store_with_admin(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $role = 'admin'; + $roles = [ + 'root', + ]; + + $user->setAttribute('role', $role); + $user->setAttribute('roles', $roles); + + $this->assertTrue($instance->store($user)); + } + + public function test_update_without_role(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $this->assertInstanceOf(Response::class, $instance->update($user, $model)); + } + + public function test_update_with_admin(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $role = 'admin'; + $roles = [ + 'root', + ]; + + $user->setAttribute('role', $role); + $user->setAttribute('roles', $roles); + + $this->assertTrue($instance->update($user, $model)); + } + + public function test_update_locked_with_admin(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $model->setAttribute('locked', true); + + $role = 'admin'; + $roles = [ + 'root', + ]; + + $user->setAttribute('role', $role); + $user->setAttribute('roles', $roles); + + $response = $instance->update($user, $model); + + $this->assertInstanceOf( + Response::class, + $response + ); + + $this->assertFalse($response->allowed()); + $this->assertTrue($response->denied()); + $this->assertSame(423, $response->status()); + + // These could be customized: + // $this->assertNull($response->code()); + // $this->assertNull($response->message()); + } + + public function test_unlock_without_role(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $this->assertInstanceOf(Response::class, $instance->unlock($user, $model)); + } + + public function test_unlock_with_admin(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $model = new UserWithRoleAndRoles; + + $role = 'admin'; + $roles = [ + 'root', + ]; + + $user->setAttribute('role', $role); + $user->setAttribute('roles', $roles); + + $this->assertTrue($instance->unlock($user, $model)); + } +} diff --git a/tests/Unit/Policies/ModelPolicy/TestPolicy.php b/tests/Unit/Policies/ModelPolicy/TestPolicy.php new file mode 100644 index 0000000..366d0aa --- /dev/null +++ b/tests/Unit/Policies/ModelPolicy/TestPolicy.php @@ -0,0 +1,14 @@ + true, + 'playground.auth.userRoles' => true, + 'playground.auth.verify' => 'roles', + ]); + } + + public function test_before_with_root(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $ability = 'edit'; + + $role = 'root'; + + $user->setAttribute('role', $role); + + $this->assertTrue($instance->before( + $user, + $ability + )); + } + + public function test_before_with_root_as_secondary_fails(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $ability = 'edit'; + + $role = 'admin'; + $roles = [ + 'root', + ]; + + $user->setAttribute('role', $role); + + $this->assertNull($instance->before( + $user, + $ability + )); + } + + public function test_index_without_role(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $this->assertInstanceOf(Response::class, $instance->index($user)); + } + + public function test_index_with_admin(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $role = 'admin'; + $roles = [ + 'root', + ]; + + $user->setAttribute('role', $role); + + $this->assertTrue($instance->index($user)); + } + + public function test_view_without_role(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $this->assertInstanceOf(Response::class, $instance->view($user)); + } + + public function test_view_with_admin(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $role = 'admin'; + $roles = [ + 'wheel', + 'user', + ]; + + $user->setAttribute('role', $role); + $user->setAttribute('roles', $roles); + + $this->assertTrue($instance->view($user)); + } + + public function test_view_with_admin_in_roles(): void + { + $instance = new TestPolicy; + + /** + * @var UserWithRoleAndRoles $user + */ + $user = UserWithRoleAndRoles::factory()->make(); + + $role = 'user-external'; + $roles = [ + 'wheel', + 'user', + ]; + + $user->setAttribute('role', $role); + $user->setAttribute('roles', $roles); + + $this->assertTrue($instance->view($user)); + } +} diff --git a/tests/Unit/Policies/Policy/TestPolicy.php b/tests/Unit/Policies/Policy/TestPolicy.php new file mode 100644 index 0000000..5654a38 --- /dev/null +++ b/tests/Unit/Policies/Policy/TestPolicy.php @@ -0,0 +1,14 @@ +assertSame('', $instance->getEntity()); + } + + public function test_getPackage(): void + { + $instance = new Policy; + $this->assertSame('', $instance->getPackage()); + } + + public function test_hasToken(): void + { + $instance = new Policy; + $this->assertFalse($instance->hasToken()); + } + + public function test_getToken(): void + { + $instance = new Policy; + $this->assertNull($instance->getToken()); + } + + public function test_setToken(): void + { + $instance = new Policy; + $this->assertIsObject($instance->setToken()); + } + + public function test_verify(): void + { + $instance = new Policy; + + $log = LogFake::bind(); + + /** + * @var User $user + */ + $user = User::factory()->make(); + + $verify = 'invalid-verifier'; + + config(['playground.auth.verify' => $verify]); + + $ability = 'view'; + + $this->assertFalse($instance->verify($user, $ability)); + + $log->assertLogged( + fn (LogEntry $log) => $log->level === 'debug' + ); + + $log->assertLogged( + fn (LogEntry $log) => str_contains( + is_string($log->context['$ability']) ? $log->context['$ability'] : '', + $ability + ) + ); + } + + public function test_verify_privileges(): void + { + $instance = new Policy; + + /** + * @var User $user + */ + $user = User::factory()->make(); + + $verify = 'privileges'; + + config(['playground.auth.verify' => $verify]); + + $ability = 'view'; + + $result = $instance->verify($user, $ability); + + $this->assertInstanceOf(Response::class, $result); + // $this->assertFalse($instance->verify($user, $ability)); + } + + public function test_verify_roles(): void + { + $instance = new Policy; + + /** + * @var User $user + */ + $user = User::factory()->make(); + + $verify = 'roles'; + + config(['playground.auth.verify' => $verify]); + + $ability = 'view'; + + $result = $instance->verify($user, $ability); + + $this->assertInstanceOf(Response::class, $result); + // $this->assertFalse($result); + } + + public function test_verify_user(): void + { + $instance = new Policy; + + /** + * @var User $user + */ + $user = User::factory()->make(); + + $verify = 'user'; + + config(['playground.auth.verify' => $verify]); + + $ability = 'view'; + + $this->assertTrue($instance->verify($user, $ability)); + } +} diff --git a/tests/Unit/Policies/PrivilegeTrait/PrivilegeModelPolicy.php b/tests/Unit/Policies/PrivilegeTrait/PrivilegeModelPolicy.php new file mode 100644 index 0000000..97866d1 --- /dev/null +++ b/tests/Unit/Policies/PrivilegeTrait/PrivilegeModelPolicy.php @@ -0,0 +1,17 @@ +assertSame($expected, $instance->privilege()); + } + + public function test_privilege_PrivilegeModelPolicy_without_parameter(): void + { + $instance = new PrivilegeModelPolicy; + + $expected = 'playground-auth:user:*'; + + $this->assertSame($expected, $instance->privilege()); + } + + public function test_privilege_UserPolicy_without_parameter(): void + { + $instance = new UserPolicy; + + $expected = '*'; + + $this->assertSame($expected, $instance->privilege()); + } + + public function test_privilege_with_package_and_without_parameter(): void + { + $instance = new PrivilegeModelPolicy; + + $expected = 'playground-auth:user:*'; + + $this->assertSame($expected, $instance->privilege()); + } + + public function test_privilege_with_package_and_entity_and_without_parameter(): void + { + $instance = new PrivilegeModelPolicy; + + $expected = 'playground-auth:user:*'; + + $this->assertSame($expected, $instance->privilege()); + } + + public function test_hasPrivilege(): void + { + $instance = new PrivilegeModelPolicy; + + config(['playground.auth.sanctum' => true]); + + /** + * @var UserWithSanctum $user + */ + $user = UserWithSanctum::factory()->make(); + + $privilege = ''; + + $this->assertInstanceOf(Response::class, $instance->hasPrivilege( + $user, + $privilege + )); + } + + public function test_hasPrivilege_with_user_hasPrivilege(): void + { + $instance = new PrivilegeModelPolicy; + + config(['playground.auth.hasPrivilege' => true]); + + /** + * @var UserWithRoleAndRolesAndPrivileges $user + */ + $user = UserWithRoleAndRolesAndPrivileges::factory()->make([ + 'privileges' => ['quack'], + ]); + $privilege = 'quack'; + + $this->assertTrue($instance->hasPrivilege( + $user, + $privilege + )); + } + + public function test_hasPrivilege_with_user_privileges(): void + { + $instance = new PrivilegeModelPolicy; + + config(['playground.auth.userPrivileges' => true]); + + /** + * @var UserWithRoleAndRolesAndPrivileges $user + */ + $user = UserWithRoleAndRolesAndPrivileges::factory()->make([ + 'privileges' => ['quack'], + ]); + $privilege = 'quack'; + + $this->assertTrue($instance->hasPrivilege( + $user, + $privilege + )); + } + + public function test_hasPrivilege_without_privileges_enabled(): void + { + $instance = new PrivilegeModelPolicy; + + config([ + 'playground.auth.hasPrivilege' => false, + 'playground.auth.userPrivileges' => false, + ]); + + /** + * @var UserWithRoleAndRolesAndPrivileges $user + */ + $user = UserWithRoleAndRolesAndPrivileges::factory()->make([ + 'privileges' => ['quack'], + ]); + $privilege = 'quack'; + + $this->assertInstanceOf(Response::class, $instance->hasPrivilege( + $user, + $privilege + )); + } +} diff --git a/tests/Unit/Policies/PrivilegeTrait/UserPolicy.php b/tests/Unit/Policies/PrivilegeTrait/UserPolicy.php new file mode 100644 index 0000000..f7c5c30 --- /dev/null +++ b/tests/Unit/Policies/PrivilegeTrait/UserPolicy.php @@ -0,0 +1,14 @@ +assertSame($expected, $instance->getRolesForAdmin()); + } + + public function test_getRolesForAction(): void + { + $instance = new RoleModelPolicy; + + $expected = [ + 'admin', + 'wheel', + 'root', + ]; + + $this->assertSame($expected, $instance->getRolesForAction()); + } + + public function test_getRolesToView(): void + { + $instance = new RoleModelPolicy; + + $expected = [ + 'admin', + 'wheel', + 'root', + ]; + + $this->assertSame($expected, $instance->getRolesToView()); + } + + public function test_hasRole(): void + { + $instance = new RoleModelPolicy; + + /** + * @var User $user + */ + $user = User::factory()->make(); + + $ability = 'edit'; + $this->assertInstanceOf(Response::class, $instance->hasRole( + $user, + $ability + )); + } + + public function test_hasRole_advanced_role(): void + { + $instance = new RoleModelPolicy; + + /** + * @var User $user + */ + $user = User::factory()->make(); + + $ability = 'some-advanded-role'; + $this->assertInstanceOf(Response::class, $instance->hasRole( + $user, + $ability + )); + } +} diff --git a/tests/Unit/TestCase.php b/tests/Unit/TestCase.php index 3d812ea..ad0f064 100644 --- a/tests/Unit/TestCase.php +++ b/tests/Unit/TestCase.php @@ -1,29 +1,22 @@ set('auth.providers.users.model', 'GammaMatrix\\Playground\\Test\\Models\\User'); + $app['config']->set('auth.providers.users.model', 'Playground\\Test\\Models\\User'); $app['config']->set('playground.auth.verify', 'user'); - $app['config']->set('playground-auth.redirect', true); - $app['config']->set('playground-auth.session', true); - - $app['config']->set('playground-auth.token.roles', false); - $app['config']->set('playground-auth.token.privileges', false); - $app['config']->set('playground-auth.token.name', 'app-testing'); - $app['config']->set('playground-auth.token.sanctum', false); - - $app['config']->set('playground-auth.load.commands', true); - $app['config']->set('playground-auth.load.routes', true); - $app['config']->set('playground-auth.load.views', true); - - $app['config']->set('playground-auth.routes.confirm', true); - $app['config']->set('playground-auth.routes.forgot', true); - $app['config']->set('playground-auth.routes.logout', true); - $app['config']->set('playground-auth.routes.login', true); - $app['config']->set('playground-auth.routes.register', true); - $app['config']->set('playground-auth.routes.reset', true); - $app['config']->set('playground-auth.routes.token', true); - $app['config']->set('playground-auth.routes.verify', true); + // $app['config']->set('playground-auth.redirect', true); + // $app['config']->set('playground-auth.session', true); - $app['config']->set('playground-auth.sitemap.enable', true); - $app['config']->set('playground-auth.sitemap.guest', true); - $app['config']->set('playground-auth.sitemap.user', true); + // $app['config']->set('playground-auth.token.roles', false); + // $app['config']->set('playground-auth.token.privileges', false); + // $app['config']->set('playground-auth.token.name', 'app-testing'); + // $app['config']->set('playground-auth.token.sanctum', false); - $app['config']->set('playground-auth.admins', []); - $app['config']->set('playground-auth.managers', []); + // $app['config']->set('playground-auth.admins', []); + // $app['config']->set('playground-auth.managers', []); } }