diff --git a/.gitignore b/.gitignore index 15324320..ad7131f0 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ junit.xml storage/*.db storage/*.sqlite .vscode +storage/oauth-public.key +storage/oauth-private.key diff --git a/app/Http/Controllers/SetupController.php b/app/Http/Controllers/SetupController.php index 34bb843a..6da12254 100644 --- a/app/Http/Controllers/SetupController.php +++ b/app/Http/Controllers/SetupController.php @@ -142,6 +142,7 @@ class SetupController extends Controller Artisan::call('salt:random'); } Artisan::call('jwt:secret', ['--no-interaction' => true]); + Artisan::call('passport:keys', ['--no-interaction' => true]); // Create tables Artisan::call('migrate', ['--force' => true]); diff --git a/app/Models/User.php b/app/Models/User.php index 67cda435..e493cc07 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,12 +5,15 @@ namespace App\Models; use DB; use Carbon\Carbon; use Illuminate\Support\Arr; +use Laravel\Passport\HasApiTokens; use App\Events\EncryptUserPassword; use Tymon\JWTAuth\Contracts\JWTSubject; use Illuminate\Foundation\Auth\User as Authenticatable; class User extends Authenticatable implements JWTSubject { + use HasApiTokens; + /** * Permissions. */ diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index dfe2f90d..d786869c 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -4,6 +4,7 @@ namespace App\Providers; use Route; use Illuminate\Routing\Router; +use Laravel\Passport\Passport; use App\Events\ConfigureRoutes; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; @@ -44,6 +45,8 @@ class RouteServiceProvider extends ServiceProvider $this->mapApiRoutes(); + Passport::routes(); + event(new ConfigureRoutes($router)); } diff --git a/app/helpers.php b/app/helpers.php index c6b0c28d..a979ba80 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -91,8 +91,14 @@ if (! function_exists('bs_menu')) { { $menu = config('menu'); - Event::dispatch($type == 'user' ? new App\Events\ConfigureUserMenu($menu) - : new App\Events\ConfigureAdminMenu($menu)); + switch ($type) { + case 'user': + event(new App\Events\ConfigureUserMenu($menu)); + break; + case 'admin': + event(new App\Events\ConfigureAdminMenu($menu)); + break; + } if (! isset($menu[$type])) { throw new InvalidArgumentException; diff --git a/composer.json b/composer.json index c0783861..4f566278 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ "mews/captcha": "^2.2", "guzzlehttp/guzzle": "^6.3", "doctrine/dbal": "^2.9", - "tymon/jwt-auth": "dev-develop" + "tymon/jwt-auth": "dev-develop", + "laravel/passport": "^7.2" }, "require-dev": { "fzaninotto/faker": "~1.4", diff --git a/composer.lock b/composer.lock index f85852f7..f54bea7e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ff9f059d35e12b8b7c52f529300ce9f2", + "content-hash": "a2b9b6f33c5d48464bf9d66c7c1c2b14", "packages": [ { "name": "composer/semver", @@ -68,6 +68,69 @@ ], "time": "2016-08-30T16:08:34+00:00" }, + { + "name": "defuse/php-encryption", + "version": "v2.2.1", + "source": { + "type": "git", + "url": "https://github.com/defuse/php-encryption.git", + "reference": "0f407c43b953d571421e0020ba92082ed5fb7620" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/defuse/php-encryption/zipball/0f407c43b953d571421e0020ba92082ed5fb7620", + "reference": "0f407c43b953d571421e0020ba92082ed5fb7620", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "paragonie/random_compat": ">= 2", + "php": ">=5.4.0" + }, + "require-dev": { + "nikic/php-parser": "^2.0|^3.0|^4.0", + "phpunit/phpunit": "^4|^5" + }, + "bin": [ + "bin/generate-defuse-key" + ], + "type": "library", + "autoload": { + "psr-4": { + "Defuse\\Crypto\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Hornby", + "email": "taylor@defuse.ca", + "homepage": "https://defuse.ca/" + }, + { + "name": "Scott Arciszewski", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "Secure PHP Encryption Library", + "keywords": [ + "aes", + "authenticated encryption", + "cipher", + "crypto", + "cryptography", + "encrypt", + "encryption", + "openssl", + "security", + "symmetric key cryptography" + ], + "time": "2018-07-24T23:27:56+00:00" + }, { "name": "devitek/yaml-translation", "version": "4.1.0", @@ -618,6 +681,52 @@ ], "time": "2018-03-08T01:11:30+00:00" }, + { + "name": "firebase/php-jwt", + "version": "v5.0.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "9984a4d3a32ae7673d6971ea00bae9d0a1abba0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/9984a4d3a32ae7673d6971ea00bae9d0a1abba0e", + "reference": "9984a4d3a32ae7673d6971ea00bae9d0a1abba0e", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": " 4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "time": "2017-06-27T22:17:23+00:00" + }, { "name": "guzzlehttp/guzzle", "version": "6.3.3", @@ -1018,6 +1127,76 @@ ], "time": "2019-03-26T17:19:10+00:00" }, + { + "name": "laravel/passport", + "version": "v7.2.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/passport.git", + "reference": "c0c3fca80d8f5af90dcbf65e62bdd1abee9ac25d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/passport/zipball/c0c3fca80d8f5af90dcbf65e62bdd1abee9ac25d", + "reference": "c0c3fca80d8f5af90dcbf65e62bdd1abee9ac25d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "firebase/php-jwt": "~3.0|~4.0|~5.0", + "guzzlehttp/guzzle": "~6.0", + "illuminate/auth": "~5.6.0|~5.7.0|~5.8.0|~5.9.0", + "illuminate/console": "~5.6.0|~5.7.0|~5.8.0|~5.9.0", + "illuminate/container": "~5.6.0|~5.7.0|~5.8.0|~5.9.0", + "illuminate/contracts": "~5.6.0|~5.7.0|~5.8.0|~5.9.0", + "illuminate/database": "~5.6.0|~5.7.0|~5.8.0|~5.9.0", + "illuminate/encryption": "~5.6.0|~5.7.0|~5.8.0|~5.9.0", + "illuminate/http": "~5.6.0|~5.7.0|~5.8.0|~5.9.0", + "illuminate/support": "~5.6.0|~5.7.0|~5.8.0|~5.9.0", + "league/oauth2-server": "^7.0", + "php": ">=7.1", + "phpseclib/phpseclib": "^2.0", + "symfony/psr-http-message-bridge": "~1.0", + "zendframework/zend-diactoros": "~1.0|~2.0" + }, + "require-dev": { + "mockery/mockery": "~1.0", + "phpunit/phpunit": "~7.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.0-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Passport\\PassportServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Passport\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Passport provides OAuth2 server support to Laravel.", + "keywords": [ + "laravel", + "oauth", + "passport" + ], + "time": "2019-03-13T14:21:06+00:00" + }, { "name": "lcobucci/jwt", "version": "3.2.5", @@ -1076,6 +1255,56 @@ ], "time": "2018-11-11T12:22:26+00:00" }, + { + "name": "league/event", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/event.git", + "reference": "d2cc124cf9a3fab2bb4ff963307f60361ce4d119" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/event/zipball/d2cc124cf9a3fab2bb4ff963307f60361ce4d119", + "reference": "d2cc124cf9a3fab2bb4ff963307f60361ce4d119", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "henrikbjorn/phpspec-code-coverage": "~1.0.1", + "phpspec/phpspec": "^2.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Event\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Event package", + "keywords": [ + "emitter", + "event", + "listener" + ], + "time": "2018-11-26T11:52:41+00:00" + }, { "name": "league/flysystem", "version": "1.0.51", @@ -1160,6 +1389,83 @@ ], "time": "2019-03-30T13:22:34+00:00" }, + { + "name": "league/oauth2-server", + "version": "7.3.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-server.git", + "reference": "c7f499849704ebe2c60b45b6d6bb231df5601d4a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/c7f499849704ebe2c60b45b6d6bb231df5601d4a", + "reference": "c7f499849704ebe2c60b45b6d6bb231df5601d4a", + "shasum": "" + }, + "require": { + "defuse/php-encryption": "^2.1", + "ext-openssl": "*", + "lcobucci/jwt": "^3.2.2", + "league/event": "^2.1", + "php": ">=7.0.0", + "psr/http-message": "^1.0.1" + }, + "replace": { + "league/oauth2server": "*", + "lncd/oauth2": "*" + }, + "require-dev": { + "phpstan/phpstan": "^0.9.2", + "phpstan/phpstan-phpunit": "^0.9.4", + "phpstan/phpstan-strict-rules": "^0.9.0", + "phpunit/phpunit": "^6.3 || ^7.0", + "roave/security-advisories": "dev-master", + "zendframework/zend-diactoros": "^1.3.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OAuth2\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Andy Millington", + "email": "andrew@noexceptions.io", + "homepage": "https://www.noexceptions.io", + "role": "Developer" + } + ], + "description": "A lightweight and powerful OAuth 2.0 authorization and resource server library with support for all the core specification grants. This library will allow you to secure your API with OAuth and allow your applications users to approve apps that want to access their data from your API.", + "homepage": "https://oauth2.thephpleague.com/", + "keywords": [ + "Authentication", + "api", + "auth", + "authorisation", + "authorization", + "oauth", + "oauth 2", + "oauth 2.0", + "oauth2", + "protect", + "resource", + "secure", + "server" + ], + "time": "2019-03-29T18:19:35+00:00" + }, { "name": "mews/captcha", "version": "2.2.5", @@ -1584,6 +1890,98 @@ ], "time": "2015-07-25T16:39:46+00:00" }, + { + "name": "phpseclib/phpseclib", + "version": "2.0.15", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "11cf67cf78dc4acb18dc9149a57be4aee5036ce0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/11cf67cf78dc4acb18dc9149a57be4aee5036ce0", + "reference": "11cf67cf78dc4acb18dc9149a57be4aee5036ce0", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phing/phing": "~2.7", + "phpunit/phpunit": "^4.8.35|^5.7|^6.0", + "sami/sami": "~2.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "suggest": { + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "time": "2019-03-10T16:53:45+00:00" + }, { "name": "predis/predis", "version": "v1.1.1", @@ -1683,6 +2081,58 @@ ], "time": "2017-02-14T16:28:37+00:00" }, + { + "name": "psr/http-factory", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "378bfe27931ecc54ff824a20d6f6bfc303bbd04c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/378bfe27931ecc54ff824a20d6f6bfc303bbd04c", + "reference": "378bfe27931ecc54ff824a20d6f6bfc303bbd04c", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "time": "2018-07-30T21:54:04+00:00" + }, { "name": "psr/http-message", "version": "1.0.1", @@ -2894,6 +3344,71 @@ "homepage": "https://symfony.com", "time": "2019-01-24T22:05:03+00:00" }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "9ab9d71f97d5c7d35a121a7fb69f74fee95cd0ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/9ab9d71f97d5c7d35a121a7fb69f74fee95cd0ad", + "reference": "9ab9d71f97d5c7d35a121a7fb69f74fee95cd0ad", + "shasum": "" + }, + "require": { + "php": "^7.1", + "psr/http-message": "^1.0", + "symfony/http-foundation": "^3.4 || ^4.0" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "symfony/phpunit-bridge": "^3.4.20 || ^4.0", + "zendframework/zend-diactoros": "^1.4.1 || ^2.0" + }, + "suggest": { + "nyholm/psr7": "For a super lightweight PSR-7/17 implementation" + }, + "type": "symfony-bridge", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "http://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-17", + "psr-7" + ], + "time": "2019-03-11T18:22:33+00:00" + }, { "name": "symfony/routing", "version": "v4.2.3", @@ -3352,6 +3867,72 @@ "environment" ], "time": "2019-01-30T10:43:17+00:00" + }, + { + "name": "zendframework/zend-diactoros", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-diactoros.git", + "reference": "c3c330192bc9cc51b7e9ce968ff721dc32ffa986" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/c3c330192bc9cc51b7e9ce968ff721dc32ffa986", + "reference": "c3c330192bc9cc51b7e9ce968ff721dc32ffa986", + "shasum": "" + }, + "require": { + "php": "^7.1", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-dom": "*", + "ext-libxml": "*", + "http-interop/http-factory-tests": "^0.5.0", + "php-http/psr7-integration-tests": "dev-master", + "phpunit/phpunit": "^7.0.2", + "zendframework/zend-coding-standard": "~1.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev", + "dev-develop": "2.2.x-dev", + "dev-release-1.8": "1.8.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/marshal_uri_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php" + ], + "psr-4": { + "Zend\\Diactoros\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "PSR HTTP Message implementations", + "keywords": [ + "http", + "psr", + "psr-7" + ], + "time": "2019-01-05T20:13:32+00:00" } ], "packages-dev": [ diff --git a/config/auth.php b/config/auth.php index 56c91baf..19dd93c5 100644 --- a/config/auth.php +++ b/config/auth.php @@ -45,6 +45,11 @@ return [ 'driver' => 'jwt', 'provider' => 'users', ], + + 'oauth' => [ + 'driver' => 'passport', + 'provider' => 'users', + ], ], /* diff --git a/config/menu.php b/config/menu.php index fcc7b63b..d2952470 100644 --- a/config/menu.php +++ b/config/menu.php @@ -14,6 +14,13 @@ $menu['user'] = [ ['title' => 'general.player-manage', 'link' => 'user/player', 'icon' => 'fa-users'], ['title' => 'general.my-reports', 'link' => 'user/reports', 'icon' => 'fa-flag'], ['title' => 'general.profile', 'link' => 'user/profile', 'icon' => 'fa-user'], + [ + 'title' => 'general.developer', + 'icon' => 'fa-code-branch', + 'children' => [ + ['title' => 'general.oauth-manage', 'link' => 'user/oauth/manage', 'icon' => 'fa-feather-alt'], + ], + ], ]; $menu['admin'] = [ diff --git a/database/update_scripts/update-4.0.4-to-4.1.0.php b/database/update_scripts/update-4.0.4-to-4.1.0.php index 1f91b563..ac9fabff 100644 --- a/database/update_scripts/update-4.0.4-to-4.1.0.php +++ b/database/update_scripts/update-4.0.4-to-4.1.0.php @@ -3,3 +3,5 @@ use Artisan; Artisan::call('jwt:secret', ['--no-interaction' => true]); +Artisan::call('migrate', ['--force' => true]); +Artisan::call('passport:keys', ['--no-interaction' => true]); diff --git a/resources/assets/src/scripts/route.ts b/resources/assets/src/scripts/route.ts index 3b386d00..a66ae257 100644 --- a/resources/assets/src/scripts/route.ts +++ b/resources/assets/src/scripts/route.ts @@ -36,6 +36,11 @@ export default [ component: () => import('../views/user/Profile.vue'), el: '.content', }, + { + path: 'user/oauth/manage', + component: () => import('../views/user/OAuth.vue'), + el: '.content', + }, { path: 'admin', module: [() => import('../views/admin/Dashboard')], diff --git a/resources/assets/src/views/user/OAuth.vue b/resources/assets/src/views/user/OAuth.vue new file mode 100644 index 00000000..1b787678 --- /dev/null +++ b/resources/assets/src/views/user/OAuth.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/resources/assets/tests/scripts/net.test.ts b/resources/assets/tests/scripts/net.test.ts index 1cfd4e73..6e7c51c4 100644 --- a/resources/assets/tests/scripts/net.test.ts +++ b/resources/assets/tests/scripts/net.test.ts @@ -5,16 +5,6 @@ import { showAjaxError } from '@/scripts/notify' jest.mock('@/scripts/notify') -;(window as Window & { Request: any }).Request = class { - headers: Map - - constructor(public url: string, init: Request) { - this.url = url - Object.assign(this, init) - this.headers = new Map(Object.entries(init.headers)) - } -} - test('the GET method', async () => { const json = jest.fn().mockResolvedValue({}) window.fetch = jest.fn().mockResolvedValue({ diff --git a/resources/assets/tests/setup.js b/resources/assets/tests/setup.js index 5c767b10..a1b36bd6 100644 --- a/resources/assets/tests/setup.js +++ b/resources/assets/tests/setup.js @@ -1,3 +1,4 @@ +/* eslint-disable max-classes-per-file */ /* eslint-disable import/no-extraneous-dependencies */ import 'jest-extended' import Vue from 'vue' @@ -20,6 +21,14 @@ window.Headers = class extends Map { } } +window.Request = class { + constructor(url, init) { + this.url = url + Object.assign(this, init) + this.headers = new Map(Object.entries(init.headers || {})) + } +} + const noop = () => undefined // eslint-disable-next-line no-console Object.keys(console).forEach(method => (console[method] = noop)) diff --git a/resources/assets/tests/views/user/OAuth.test.ts b/resources/assets/tests/views/user/OAuth.test.ts new file mode 100644 index 00000000..22be1d67 --- /dev/null +++ b/resources/assets/tests/views/user/OAuth.test.ts @@ -0,0 +1,138 @@ +import Vue from 'vue' +import { mount } from '@vue/test-utils' +import { MessageBoxData } from 'element-ui/types/message-box' +import { flushPromises } from '../../utils' +import { walkFetch } from '@/scripts/net' +import OAuth from '@/views/user/OAuth.vue' + +jest.mock('@/scripts/net', () => ({ + walkFetch: jest.fn(), + init: {}, +})) + +test('basic render', async () => { + Vue.prototype.$http.get.mockResolvedValue([ + { id: 1 }, + ]) + const wrapper = mount(OAuth) + await wrapper.vm.$nextTick() + expect(wrapper.findAll('[data-test=remove]')).toHaveLength(1) +}) + +test('create app', async () => { + Object.assign(window, { $: () => ({ modal() {} }) }) + Vue.prototype.$http.get.mockResolvedValue([]) + Vue.prototype.$http.post + .mockResolvedValueOnce({ message: 'fail' }) + .mockResolvedValueOnce({ id: 1, name: 'name' }) + const wrapper = mount(OAuth) + await wrapper.vm.$nextTick() + + const button = wrapper.find('[data-test=create]') + const inputs = wrapper.findAll('.value') + inputs.at(0).find('input') + .setValue('name') + inputs.at(1).find('input') + .setValue('https://example.com/') + + button.trigger('click') + await wrapper.vm.$nextTick() + expect(Vue.prototype.$http.post).toBeCalledWith( + '/oauth/clients', + { name: 'name', redirect: 'https://example.com/' } + ) + expect(Vue.prototype.$message.warning).toBeCalledWith('fail') + + button.trigger('click') + await wrapper.vm.$nextTick() + expect(wrapper.text()).toContain('name') +}) + +test('modify name', async () => { + Vue.prototype.$http.get.mockResolvedValue([ + { id: 1, name: 'old' }, + ]) + walkFetch + .mockResolvedValueOnce({ message: 'fail' }) + .mockResolvedValueOnce({ id: 1, name: 'new-name' }) + Vue.prototype.$prompt + .mockRejectedValueOnce('') + .mockResolvedValue({ value: 'new-name' } as MessageBoxData) + const wrapper = mount(OAuth) + await wrapper.vm.$nextTick() + const button = wrapper.find('[data-test=name]') + + button.trigger('click') + await wrapper.vm.$nextTick() + expect(walkFetch).not.toBeCalled() + + button.trigger('click') + await flushPromises() + expect(walkFetch).toBeCalledWith( + expect.objectContaining({ + url: '/oauth/clients/1', + body: JSON.stringify({ name: 'new-name' }), + method: 'PUT', + }) + ) + expect(Vue.prototype.$message.warning).toBeCalledWith('fail') + + button.trigger('click') + await flushPromises() + expect(wrapper.text()).toContain('new-name') +}) + +test('modify redirect', async () => { + Vue.prototype.$http.get.mockResolvedValue([ + { id: 1, redirect: 'https://example.com/' }, + ]) + walkFetch + .mockResolvedValueOnce({ message: 'fail' }) + .mockResolvedValueOnce({ id: 1, redirect: 'https://example.net/' }) + Vue.prototype.$prompt + .mockRejectedValueOnce('') + .mockResolvedValue({ value: 'https://example.net/' } as MessageBoxData) + const wrapper = mount(OAuth) + await wrapper.vm.$nextTick() + const button = wrapper.find('[data-test=callback]') + + button.trigger('click') + await wrapper.vm.$nextTick() + expect(walkFetch).not.toBeCalled() + + button.trigger('click') + await flushPromises() + expect(walkFetch).toBeCalledWith( + expect.objectContaining({ + url: '/oauth/clients/1', + body: JSON.stringify({ redirect: 'https://example.net/' }), + method: 'PUT', + }) + ) + expect(Vue.prototype.$message.warning).toBeCalledWith('fail') + + button.trigger('click') + await flushPromises() + expect(wrapper.text()).toContain('https://example.net/') +}) + +test('remove app', async () => { + Vue.prototype.$http.get.mockResolvedValue([ + { id: 1, name: 'name' }, + ]) + Vue.prototype.$confirm + .mockRejectedValueOnce('cancel') + .mockResolvedValue('confirm') + + const wrapper = mount(OAuth) + await wrapper.vm.$nextTick() + const button = wrapper.find('[data-test=remove]') + + button.trigger('click') + await flushPromises() + expect(walkFetch).not.toBeCalled() + + button.trigger('click') + await flushPromises() + expect(wrapper.text()).toContain('No data') +}) diff --git a/resources/lang/en/auth.yml b/resources/lang/en/auth.yml index bcdd4482..8030094d 100644 --- a/resources/lang/en/auth.yml +++ b/resources/lang/en/auth.yml @@ -78,6 +78,12 @@ logout: success: You are now logged out. fail: No valid session. +oauth: + authorization: + title: Authorization + introduction: A 3rd-party application ":name" is requesting permission to access your account. + button: Authorize + nickname: Nickname email: Email identification: Email or player name diff --git a/resources/lang/en/front-end.yml b/resources/lang/en/front-end.yml index 8f06b1ff..2a4f8192 100644 --- a/resources/lang/en/front-end.yml +++ b/resources/lang/en/front-end.yml @@ -223,6 +223,15 @@ user: message: You must verify your email address before using the skin hosting service. Haven't received the email? resend: Click here to send again. sending: Sending... + oauth: + id: Client ID + name: App Name + secret: Client Secret + redirect: Callback URL + modifyName: Modify app name. + modifyUrl: Modify callback URL. + create: Create New App + confirmRemove: Are you sure to delete this app? You won't be able to undo this. admin: operationsTitle: Operations diff --git a/resources/lang/en/general.yml b/resources/lang/en/general.yml index ba727e6d..9d988441 100644 --- a/resources/lang/en/general.yml +++ b/resources/lang/en/general.yml @@ -13,6 +13,8 @@ back: Back dashboard: Dashboard my-closet: Closet my-reports: Reports +developer: Developers +oauth-manage: OAuth2 Apps player-manage: Players user-manage: Users report-manage: Reports @@ -39,6 +41,7 @@ pause: Pause reset: Reset submit: Submit +cancel: Cancel op-success: Operated successfully. notice: Notice diff --git a/resources/lang/zh_CN/auth.yml b/resources/lang/zh_CN/auth.yml index c6b8859b..dc753085 100644 --- a/resources/lang/zh_CN/auth.yml +++ b/resources/lang/zh_CN/auth.yml @@ -78,6 +78,12 @@ logout: success: 登出成功 fail: 未找到已保存的登录信息 +oauth: + authorization: + title: 授权 + introduction: 第三方应用 :name 正在向您请求获取权限。 + button: 授权 + nickname: 昵称 email: Email identification: Email 或角色名 diff --git a/resources/lang/zh_CN/front-end.yml b/resources/lang/zh_CN/front-end.yml index 5a130087..f8271d0b 100644 --- a/resources/lang/zh_CN/front-end.yml +++ b/resources/lang/zh_CN/front-end.yml @@ -221,6 +221,15 @@ user: message: 你必须验证你的邮箱才能正常使用本站的皮肤托管等功能。没有收到验证邮件? resend: 点击这里再次发送。 sending: 正在发送…… + oauth: + id: 客户端 ID + name: 应用名 + secret: 客户端 Secret + redirect: 回调 URL + modifyName: 更改应用名 + modifyUrl: 更改回调 URL + create: 创建应用 + confirmRemove: 确认要删除这个应用吗?此操作不可撤销。 admin: operationsTitle: 更多操作 diff --git a/resources/lang/zh_CN/general.yml b/resources/lang/zh_CN/general.yml index db9fb88e..af5ed015 100644 --- a/resources/lang/zh_CN/general.yml +++ b/resources/lang/zh_CN/general.yml @@ -13,6 +13,8 @@ back: 返回 dashboard: 仪表盘 my-closet: 我的衣柜 my-reports: 我的举报 +developer: 开发者 +oauth-manage: OAuth2 应用 player-manage: 角色管理 user-manage: 用户管理 report-manage: 举报管理 @@ -39,6 +41,7 @@ pause: 暂停 reset: 重置 submit: 提交 +cancel: 取消 op-success: 操作成功 notice: 提示 diff --git a/resources/views/user/oauth.blade.php b/resources/views/user/oauth.blade.php new file mode 100644 index 00000000..3072f00e --- /dev/null +++ b/resources/views/user/oauth.blade.php @@ -0,0 +1,12 @@ +@extends('user.master') + +@section('title', trans('general.oauth-manage')) + +@section('content') +
+
+

@lang('general.oauth-manage')

+
+
+
+@endsection diff --git a/resources/views/vendor/passport/authorize.blade.php b/resources/views/vendor/passport/authorize.blade.php new file mode 100644 index 00000000..4c8350f4 --- /dev/null +++ b/resources/views/vendor/passport/authorize.blade.php @@ -0,0 +1,39 @@ +@extends('auth.master') + +@section('title', trans('auth.oauth.authorization.title')) + +@section('content') +
+ + + +
+@endsection diff --git a/routes/api.php b/routes/api.php index d08c8146..4ab06c96 100644 --- a/routes/api.php +++ b/routes/api.php @@ -6,11 +6,11 @@ Route::prefix('auth')->group(function () { Route::post('refresh', 'AuthController@jwtRefresh')->middleware('auth:jwt'); }); -Route::prefix('user')->middleware('auth:jwt')->group(function () { +Route::prefix('user')->middleware('auth:jwt,oauth')->group(function () { Route::put('sign', 'UserController@sign'); }); -Route::prefix('players')->middleware('auth:jwt')->group(function () { +Route::prefix('players')->middleware('auth:jwt,oauth')->group(function () { Route::get('', 'PlayerController@listAll'); Route::post('', 'PlayerController@add'); Route::delete('{pid}', 'PlayerController@delete'); diff --git a/routes/web.php b/routes/web.php index af915933..97213d63 100644 --- a/routes/web.php +++ b/routes/web.php @@ -82,6 +82,9 @@ Route::group([ Route::post('/closet/add', 'ClosetController@add'); Route::post('/closet/remove', 'ClosetController@remove'); Route::post('/closet/rename', 'ClosetController@rename'); + + // OAuth2 Management + Route::view('/oauth/manage', 'user.oauth'); }); /* diff --git a/tests/SetupControllerTest.php b/tests/SetupControllerTest.php index 8c15e913..658e8f4f 100644 --- a/tests/SetupControllerTest.php +++ b/tests/SetupControllerTest.php @@ -83,9 +83,12 @@ class SetupControllerTest extends TestCase public function testInfo() { - $this->get('/setup/info') - ->assertViewIs('setup.wizard.info'); - + $this->get('/setup/info')->assertViewIs('setup.wizard.info'); + Schema::dropIfExists('oauth_auth_codes'); + Schema::dropIfExists('oauth_access_tokens'); + Schema::dropIfExists('oauth_clients'); + Schema::dropIfExists('oauth_personal_access_clients'); + Schema::dropIfExists('oauth_refresh_tokens'); Artisan::call('migrate:refresh'); Schema::drop('users'); $this->get('/setup/info')->assertSee('already exist'); @@ -167,6 +170,10 @@ class SetupControllerTest extends TestCase ->with('jwt:secret', ['--no-interaction' => true]) ->once() ->andReturn(true); + Artisan::shouldReceive('call') + ->with('passport:keys', ['--no-interaction' => true]) + ->once() + ->andReturn(true); Artisan::shouldReceive('call') ->with('migrate', ['--force' => true]) ->once()