From 47c1ecc9bcc54f03d6d4d5238ec19254a2978a95 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 28 May 2018 11:34:24 -0700 Subject: [PATCH 1/4] Make server blocks clickable, break out routes into their own file --- resources/assets/scripts/app.js | 26 ++------------- .../components/dashboard/Dashboard.vue | 10 +++--- resources/assets/scripts/routes.js | 33 +++++++++++++++++++ .../styles/components/miscellaneous.css | 6 +++- 4 files changed, 46 insertions(+), 29 deletions(-) create mode 100644 resources/assets/scripts/routes.js diff --git a/resources/assets/scripts/app.js b/resources/assets/scripts/app.js index 15fb4af18..d2a5be98e 100644 --- a/resources/assets/scripts/app.js +++ b/resources/assets/scripts/app.js @@ -7,12 +7,7 @@ import VueRouter from 'vue-router'; import { Ziggy } from './helpers/ziggy'; import Locales from './../../../resources/lang/locales'; import { flash } from './mixins/flash'; - -// 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 { routes } from './routes'; window.events = new Vue; window.Ziggy = Ziggy; @@ -33,24 +28,7 @@ Vue.i18n.add('en', Locales.en); Vue.i18n.set('en'); 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 }, - ] + mode: 'history', routes }); require('./bootstrap'); diff --git a/resources/assets/scripts/components/dashboard/Dashboard.vue b/resources/assets/scripts/components/dashboard/Dashboard.vue index 1da8cf667..f12f2fd02 100644 --- a/resources/assets/scripts/components/dashboard/Dashboard.vue +++ b/resources/assets/scripts/components/dashboard/Dashboard.vue @@ -9,12 +9,14 @@
-
+
-
+
-
{{ server.name }}
+
+ {{ server.name }} +
@@ -40,7 +42,7 @@

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

-
+
diff --git a/resources/assets/scripts/routes.js b/resources/assets/scripts/routes.js new file mode 100644 index 000000000..64b255463 --- /dev/null +++ b/resources/assets/scripts/routes.js @@ -0,0 +1,33 @@ +// 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'; + +export const 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 }, + + { + name: 'server', + path: '/server/:id', + // component: Server, + // children: [ + // { path: 'files', component: ServerFileManager } + // ], + } +]; diff --git a/resources/assets/styles/components/miscellaneous.css b/resources/assets/styles/components/miscellaneous.css index 9007161bf..23bec3473 100644 --- a/resources/assets/styles/components/miscellaneous.css +++ b/resources/assets/styles/components/miscellaneous.css @@ -65,7 +65,11 @@ code { } & > .content { - @apply .border .border-grey-light .bg-white .rounded .p-4 .justify-between .leading-normal; + @apply .border .border-grey-light .bg-white .rounded .p-4 .justify-between .leading-normal .no-underline .block .text-black; + + &:visited { + @apply .text-black; + } } } From ad69193ac0fe314810db53acef603b878292378a Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 28 May 2018 12:48:42 -0700 Subject: [PATCH 2/4] Add JWT to login forms --- app/Http/Controllers/Auth/LoginController.php | 21 ++++++++++-- app/Http/Middleware/Api/AuthenticateKey.php | 1 + resources/assets/scripts/app.js | 4 +-- resources/assets/scripts/bootstrap.js | 1 + .../scripts/components/auth/LoginForm.vue | 8 +++-- .../scripts/components/auth/TwoFactorForm.vue | 3 ++ resources/assets/scripts/models/user.js | 33 +++++++++++++++++++ resources/assets/scripts/store.js | 28 ++++++++++++++++ 8 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 resources/assets/scripts/models/user.js create mode 100644 resources/assets/scripts/store.js 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..774fb930b 100644 --- a/app/Http/Middleware/Api/AuthenticateKey.php +++ b/app/Http/Middleware/Api/AuthenticateKey.php @@ -63,6 +63,7 @@ class AuthenticateKey } $raw = $request->bearerToken(); + $identifier = substr($raw, 0, ApiKey::IDENTIFIER_LENGTH); $token = substr($raw, ApiKey::IDENTIFIER_LENGTH); diff --git a/resources/assets/scripts/app.js b/resources/assets/scripts/app.js index d2a5be98e..204ae6e1c 100644 --- a/resources/assets/scripts/app.js +++ b/resources/assets/scripts/app.js @@ -8,13 +8,13 @@ import { Ziggy } from './helpers/ziggy'; import Locales from './../../../resources/lang/locales'; import { flash } from './mixins/flash'; 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; diff --git a/resources/assets/scripts/bootstrap.js b/resources/assets/scripts/bootstrap.js index 8d2009067..24ea8c7f0 100644 --- a/resources/assets/scripts/bootstrap.js +++ b/resources/assets/scripts/bootstrap.js @@ -19,6 +19,7 @@ try { window.axios = require('axios'); window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; +window.axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.token || ''; /** * 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/models/user.js b/resources/assets/scripts/models/user.js new file mode 100644 index 000000000..c76123779 --- /dev/null +++ b/resources/assets/scripts/models/user.js @@ -0,0 +1,33 @@ +import JwtDecode from 'jwt-decode'; + +const User = function () { + this.id = 0; + this.admin = false; + this.email = ''; +}; + +/** + * Return a new instance of the user model using a JWT. + * + * @param {string} token + * @returns {User} + */ +User.prototype.fromJwt = function (token) { + return this.newModel(JwtDecode(token)); +}; + +/** + * Return an instance of this user model with the properties set on it. + * + * @param {object} obj + * @returns {User} + */ +User.prototype.newModel = function (obj) { + this.id = obj.id; + this.admin = obj.admin; + this.email = obj.email; + + return this; +}; + +export default User; diff --git a/resources/assets/scripts/store.js b/resources/assets/scripts/store.js new file mode 100644 index 000000000..f0e1da067 --- /dev/null +++ b/resources/assets/scripts/store.js @@ -0,0 +1,28 @@ +import User from './models/user'; + +export const storeData = { + state: { + user: null, + }, + actions: { + login: function ({ commit }) { + commit('login'); + }, + logout: function ({ commit }) { + commit('logout'); + }, + }, + getters: { + user: function (state) { + return state.user; + }, + }, + mutations: { + login: function (state) { + state.user = new User().fromJwt(localStorage.getItem('token')); + }, + logout: function (state) { + state.user = null; + } + } +}; From 6e5c365018e51d17641f96445bf2974f67d5c74f Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 28 May 2018 13:23:40 -0700 Subject: [PATCH 3/4] Use the client API to load servers on the listing page --- .../Api/Client/ClientController.php | 4 +- app/Http/Middleware/Api/AuthenticateKey.php | 17 ++++++++ app/Http/Middleware/Api/SetSessionDriver.php | 5 --- .../Api/Client/ServerTransformer.php | 4 ++ resources/assets/scripts/bootstrap.js | 8 ++++ .../components/dashboard/Dashboard.vue | 10 +++-- resources/assets/scripts/models/allocation.js | 19 +++++++++ resources/assets/scripts/models/server.js | 40 +++++++++++++++++++ 8 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 resources/assets/scripts/models/allocation.js create mode 100644 resources/assets/scripts/models/server.js 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/Middleware/Api/AuthenticateKey.php b/app/Http/Middleware/Api/AuthenticateKey.php index 774fb930b..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; @@ -64,6 +65,22 @@ 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..c44630b5c 100644 --- a/app/Transformers/Api/Client/ServerTransformer.php +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -29,6 +29,10 @@ class ServerTransformer extends BaseClientTransformer 'uuid' => $server->uuid, 'name' => $server->name, 'description' => $server->description, + 'allocation' => [ + 'ip' => $server->allocation->alias, + 'port' => $server->allocation->port, + ], 'limits' => [ 'memory' => $server->memory, 'swap' => $server->swap, diff --git a/resources/assets/scripts/bootstrap.js b/resources/assets/scripts/bootstrap.js index 24ea8c7f0..562a7adf3 100644 --- a/resources/assets/scripts/bootstrap.js +++ b/resources/assets/scripts/bootstrap.js @@ -21,6 +21,14 @@ 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 * all outgoing HTTP requests automatically have it attached. This is just diff --git a/resources/assets/scripts/components/dashboard/Dashboard.vue b/resources/assets/scripts/components/dashboard/Dashboard.vue index f12f2fd02..5ee5199ce 100644 --- a/resources/assets/scripts/components/dashboard/Dashboard.vue +++ b/resources/assets/scripts/components/dashboard/Dashboard.vue @@ -9,7 +9,7 @@
- +
@@ -49,6 +49,7 @@