diff --git a/app/Http/Controllers/Api/Client/ClientController.php b/app/Http/Controllers/Api/Client/ClientController.php index d2e1f33a9..62b5d2c4a 100644 --- a/app/Http/Controllers/Api/Client/ClientController.php +++ b/app/Http/Controllers/Api/Client/ClientController.php @@ -35,7 +35,9 @@ class ClientController extends ClientApiController */ public function index(GetServersRequest $request): array { - $servers = $this->repository->filterUserAccessServers($request->user(), User::FILTER_LEVEL_SUBUSER); + $servers = $this->repository + ->setSearchTerm($request->input('query')) + ->filterUserAccessServers($request->user(), User::FILTER_LEVEL_ALL); return $this->fractal->collection($servers) ->transformWith($this->getTransformer(ServerTransformer::class)) diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 0a6ee3176..96549a628 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -2,9 +2,11 @@ namespace Pterodactyl\Http\Controllers\Auth; +use Lcobucci\JWT\Builder; use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; use Illuminate\Contracts\View\View; +use Lcobucci\JWT\Signer\Hmac\Sha256; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; class LoginController extends AbstractLoginController @@ -63,11 +65,26 @@ class LoginController extends AbstractLoginController 'request_ip' => $request->ip(), ], 5); - return response()->json(['complete' => false, 'token' => $token]); + return response()->json(['complete' => false, 'login_token' => $token]); } + $signer = new Sha256(); + $token = (new Builder)->setIssuer('http://pterodactyl.local') + ->setAudience('http://pterodactyl.local') + ->setId(str_random(12), true) + ->setIssuedAt(time()) + ->setNotBefore(time()) + ->setExpiration(time() + 3600) + ->set('uid', $user->id) + ->sign($signer, env('APP_JWT_KEY')) + ->getToken(); + $this->auth->guard()->login($user, true); - return response()->json(['complete' => true]); + return response()->json([ + 'complete' => true, + 'intended' => $this->redirectPath(), + 'token' => $token->__toString(), + ]); } } diff --git a/app/Http/Middleware/Api/AuthenticateKey.php b/app/Http/Middleware/Api/AuthenticateKey.php index 8f400bb4d..3ae04f6fe 100644 --- a/app/Http/Middleware/Api/AuthenticateKey.php +++ b/app/Http/Middleware/Api/AuthenticateKey.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Http\Middleware\Api; use Closure; +use Lcobucci\JWT\Parser; use Cake\Chronos\Chronos; use Illuminate\Http\Request; use Pterodactyl\Models\ApiKey; @@ -63,6 +64,23 @@ class AuthenticateKey } $raw = $request->bearerToken(); + + // This is an internal JWT, treat it differently to get the correct user + // before passing it along. + if (strlen($raw) > ApiKey::IDENTIFIER_LENGTH + ApiKey::KEY_LENGTH) { + $token = (new Parser)->parse($raw); + + $model = (new ApiKey)->fill([ + 'user_id' => $token->getClaim('uid'), + 'key_type' => ApiKey::TYPE_ACCOUNT, + ]); + + $this->auth->guard()->loginUsingId($token->getClaim('uid')); + $request->attributes->set('api_key', $model); + + return $next($request); + } + $identifier = substr($raw, 0, ApiKey::IDENTIFIER_LENGTH); $token = substr($raw, ApiKey::IDENTIFIER_LENGTH); diff --git a/app/Http/Middleware/Api/SetSessionDriver.php b/app/Http/Middleware/Api/SetSessionDriver.php index 3d5c16617..e61604dbd 100644 --- a/app/Http/Middleware/Api/SetSessionDriver.php +++ b/app/Http/Middleware/Api/SetSessionDriver.php @@ -4,7 +4,6 @@ namespace Pterodactyl\Http\Middleware\Api; use Closure; use Illuminate\Http\Request; -use Barryvdh\Debugbar\LaravelDebugbar; use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Config\Repository as ConfigRepository; @@ -41,10 +40,6 @@ class SetSessionDriver */ public function handle(Request $request, Closure $next) { - if ($this->config->get('app.debug')) { - $this->app->make(LaravelDebugbar::class)->disable(); - } - $this->config->set('session.driver', 'array'); return $next($request); diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php index 6816d6d74..64282686a 100644 --- a/app/Transformers/Api/Client/ServerTransformer.php +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -28,7 +28,12 @@ class ServerTransformer extends BaseClientTransformer 'identifier' => $server->uuidShort, 'uuid' => $server->uuid, 'name' => $server->name, + 'node' => $server->node->name, 'description' => $server->description, + 'allocation' => [ + 'ip' => $server->allocation->alias, + 'port' => $server->allocation->port, + ], 'limits' => [ 'memory' => $server->memory, 'swap' => $server->swap, diff --git a/composer.json b/composer.json index d2ac885f0..d922ebad4 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "laracasts/utilities": "^3.0", "laravel/framework": "5.6.*", "laravel/tinker": "^1.0", + "lcobucci/jwt": "^3.2", "lord/laroute": "^2.4", "matriphe/iso-639": "^1.2", "nesbot/carbon": "^1.22", diff --git a/composer.lock b/composer.lock index bbb170e99..80cf8495e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "9232ff40da15c9430731254edc662eb7", + "content-hash": "f84af54d009a128472ca7e19a50fccf8", "packages": [ { "name": "appstract/laravel-blade-directives", @@ -1569,6 +1569,64 @@ ], "time": "2018-05-17T13:42:07+00:00" }, + { + "name": "lcobucci/jwt", + "version": "3.2.2", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "0b5930be73582369e10c4d4bb7a12bac927a203c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/0b5930be73582369e10c4d4bb7a12bac927a203c", + "reference": "0b5930be73582369e10c4d4bb7a12bac927a203c", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=5.5" + }, + "require-dev": { + "mdanter/ecc": "~0.3.1", + "mikey179/vfsstream": "~1.5", + "phpmd/phpmd": "~2.2", + "phpunit/php-invoker": "~1.1", + "phpunit/phpunit": "~4.5", + "squizlabs/php_codesniffer": "~2.3" + }, + "suggest": { + "mdanter/ecc": "Required to use Elliptic Curves based algorithms." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Otávio Cobucci Oblonczyk", + "email": "lcobucci@gmail.com", + "role": "developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "time": "2017-09-01T08:23:26+00:00" + }, { "name": "league/flysystem", "version": "1.0.45", diff --git a/package-lock.json b/package-lock.json index 61519d9ae..93d9a0288 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6122,6 +6122,12 @@ "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=", "dev": true }, + "jwt-decode": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", + "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=", + "dev": true + }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -7006,6 +7012,12 @@ "integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE=", "dev": true }, + "moment": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz", + "integrity": "sha512-shJkRTSebXvsVqk56I+lkb2latjBs8I+pc2TzWc545y2iFnSjm7Wg0QMh+ZWcdSLQyGEau5jI8ocnmkyTgr9YQ==", + "dev": true + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -11075,12 +11087,24 @@ "spdx-expression-parse": "3.0.0" } }, + "validator": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-8.2.0.tgz", + "integrity": "sha512-Yw5wW34fSv5spzTXNkokD6S6/Oq92d8q/t14TqsS3fAiA1RYnxSFSIZ+CY3n6PGGRCq5HhJTSepQvFUS2QUDxA==", + "dev": true + }, "value-or-function": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=", "dev": true }, + "vee-validate": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/vee-validate/-/vee-validate-2.0.9.tgz", + "integrity": "sha512-0qA3hrpF2jIBoEReWF8YkvG1ukJVS56+oyPTxOtb2OfB5d7iUuQiyboOOpXOvOViREHNXTsIcQ5XIQOMBff/wg==", + "dev": true + }, "vendors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.2.tgz", @@ -11241,6 +11265,31 @@ } } }, + "vue-mc": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/vue-mc/-/vue-mc-0.2.4.tgz", + "integrity": "sha1-k1actuCOLRxSlop0zOimssm9pmo=", + "dev": true, + "requires": { + "axios": "0.16.2", + "lodash": "4.17.5", + "moment": "2.22.1", + "validator": "8.2.0", + "vue": "2.5.16" + }, + "dependencies": { + "axios": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.16.2.tgz", + "integrity": "sha1-uk+S8XFn37q0CYN4VFS5rBScPG0=", + "dev": true, + "requires": { + "follow-redirects": "1.5.0", + "is-buffer": "1.1.6" + } + } + } + }, "vue-router": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.0.1.tgz", diff --git a/package.json b/package.json index 0f9ba4943..c9591d97e 100644 --- a/package.json +++ b/package.json @@ -24,16 +24,19 @@ "gulp-rev": "^8.1.1", "gulp-uglify-es": "^1.0.1", "jquery": "^3.3.1", + "jwt-decode": "^2.2.0", "lodash": "^4.17.5", "postcss": "^6.0.21", "postcss-import": "^11.1.0", "postcss-preset-env": "^3.4.0", "postcss-scss": "^1.0.4", "tailwindcss": "^0.5.1", + "vee-validate": "^2.0.9", "vue": "^2.5.7", "vue-axios": "^2.1.1", "vue-devtools": "^3.1.9", "vue-loader": "^14.2.2", + "vue-mc": "^0.2.4", "vue-router": "^3.0.1", "vue-template-compiler": "^2.5.16", "vueify-insert-css": "^1.0.0", diff --git a/resources/assets/scripts/app.js b/resources/assets/scripts/app.js index a6fcead1f..6c214f0dd 100644 --- a/resources/assets/scripts/app.js +++ b/resources/assets/scripts/app.js @@ -13,19 +13,14 @@ import faSolid from '@fortawesome/fontawesome-free-solid'; import FontAwesomeIcon from '@fortawesome/vue-fontawesome'; fontawesome.library.add(faSolid); -// Base Vuejs Templates -import Login from './components/auth/Login'; -import Dashboard from './components/dashboard/Dashboard'; -import Account from './components/dashboard/Account'; -import ResetPassword from './components/auth/ResetPassword'; -import { Server, ServerConsole, ServerAllocations, ServerDatabases, ServerFiles, ServerSchedules, ServerSettings, ServerSubusers } from './components/server'; +import { routes } from './routes'; +import { storeData } from './store'; window.events = new Vue; window.Ziggy = Ziggy; Vue.use(Vuex); - -const store = new Vuex.Store(); +const store = new Vuex.Store(storeData); const route = require('./../../../vendor/tightenco/ziggy/src/js/route').default; Vue.config.productionTip = false; @@ -41,35 +36,7 @@ Vue.i18n.set('en'); Vue.component('font-awesome-icon', FontAwesomeIcon); const router = new VueRouter({ - mode: 'history', - routes: [ - { name: 'login', path: '/auth/login', component: Login }, - { name: 'forgot-password', path: '/auth/password', component: Login }, - { name: 'checkpoint', path: '/checkpoint', component: Login }, - { - name: 'reset-password', - path: '/auth/password/reset/:token', - component: ResetPassword, - props: function (route) { - return { token: route.params.token, email: route.query.email || '' }; - } - }, - { name : 'index', path: '/', component: Dashboard }, - { name : 'account', path: '/account', component: Account }, - { name : 'account-api', path: '/account/api', component: Account }, - { name : 'account-security', path: '/account/security', component: Account }, - { path: '/server/:id', component: Server, - children: [ - { name: 'server', path: '', component: ServerConsole }, - { name: 'server-files', path: 'files', component: ServerFiles }, - { name: 'server-subusers', path: 'subusers', component: ServerSubusers }, - { name: 'server-schedules', path: 'schedules', component: ServerSchedules }, - { name: 'server-databases', path: 'databases', component: ServerDatabases }, - { name: 'server-allocations', path: 'allocations', component: ServerAllocations }, - { name: 'server-settings', path: 'settings', component: ServerSettings }, - ] - } - ] + mode: 'history', routes }); require('./bootstrap'); diff --git a/resources/assets/scripts/bootstrap.js b/resources/assets/scripts/bootstrap.js index 8d2009067..562a7adf3 100644 --- a/resources/assets/scripts/bootstrap.js +++ b/resources/assets/scripts/bootstrap.js @@ -19,6 +19,15 @@ try { window.axios = require('axios'); window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; +window.axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.token || ''; + +if (typeof phpdebugbar !== 'undefined') { + window.axios.interceptors.response.use(function (response) { + phpdebugbar.ajaxHandler.handle(response.request); + + return response; + }); +} /** * Next we will register the CSRF Token as a common header with Axios so that diff --git a/resources/assets/scripts/components/auth/LoginForm.vue b/resources/assets/scripts/components/auth/LoginForm.vue index 07236d550..bd217c26e 100644 --- a/resources/assets/scripts/components/auth/LoginForm.vue +++ b/resources/assets/scripts/components/auth/LoginForm.vue @@ -82,16 +82,20 @@ }) .then(function (response) { if (response.data.complete) { - return window.location = '/'; + localStorage.setItem('token', response.data.token); + self.$store.dispatch('login'); + return window.location = response.data.intended; } self.$props.user.password = ''; self.$data.showSpinner = false; - self.$router.push({name: 'checkpoint', query: {token: response.data.token}}); + self.$router.push({name: 'checkpoint', query: {token: response.data.login_token}}); }) .catch(function (err) { self.$props.user.password = ''; self.$data.showSpinner = false; + self.$store.dispatch('logout'); + if (!err.response) { return console.error(err); } diff --git a/resources/assets/scripts/components/auth/TwoFactorForm.vue b/resources/assets/scripts/components/auth/TwoFactorForm.vue index 27d2b2282..e42df8451 100644 --- a/resources/assets/scripts/components/auth/TwoFactorForm.vue +++ b/resources/assets/scripts/components/auth/TwoFactorForm.vue @@ -49,9 +49,12 @@ authentication_code: this.$data.code, }) .then(function (response) { + localStorage.setItem('token', response.data.token); + self.$store.dispatch('login'); window.location = response.data.intended; }) .catch(function (err) { + self.$store.dispatch('logout'); if (!err.response) { return console.error(err); } diff --git a/resources/assets/scripts/components/dashboard/Dashboard.vue b/resources/assets/scripts/components/dashboard/Dashboard.vue index 571e1f9ea..f49185008 100644 --- a/resources/assets/scripts/components/dashboard/Dashboard.vue +++ b/resources/assets/scripts/components/dashboard/Dashboard.vue @@ -7,11 +7,19 @@ ref="search" /> - - -
-
-
+
+ +
+
+
+
+
+ {{ server.name }} +
+
+
+
+
CPU
{{ server.name }}
@@ -34,19 +42,21 @@ Mb
-
-
-

{{ server.node_name }}

-

{{ server.allocation.ip }}:{{ server.allocation.port }}

-
+
+
+
+

{{ server.node }}

+

{{ server.allocation.ip }}:{{ server.allocation.port }}

-
+ +