Merge branch 'feature/vuejs' into feature/vue-serverview

This commit is contained in:
Jakob Schrettenbrunner 2018-06-11 21:06:12 +02:00
commit 05478e3277
29 changed files with 2997 additions and 16217 deletions

View file

@ -21,6 +21,12 @@ debconf-set-selections <<< 'mariadb-server-5.5 mysql-server/root_password_again
# actually install
apt-get install -y php7.2 php7.2-cli php7.2-gd php7.2-mysql php7.2-pdo php7.2-mbstring php7.2-tokenizer php7.2-bcmath php7.2-xml php7.2-fpm php7.2-memcached php7.2-curl php7.2-zip php-xdebug mariadb-server nginx curl tar unzip git memcached > /dev/null
echo "Install nodejs and yarn"
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
apt-get -y install nodejs yarn > /dev/null
echo "Install composer"
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

View file

@ -1,10 +1,57 @@
# Building Assets
# Local Development
Pterodactyl is now powered by Vuejs and Tailwindcss and uses webpack at its core to generate compiled assets. Release
versions of Pterodactyl will include pre-compiled, minified, and hashed assets ready-to-go.
```
However, if you are interested in running custom themes or making modifications to the Vue files you'll need a build
system in place to generate these compiled assets. To get your environment setup, you'll first need to install at least Nodejs
`8`, and it is _highly_ recommended that you also install [Yarn](https://yarnpkg.com) to manage your `node_modules`.
### Install Dependencies
```bash
yarn install
php artisan vue-i18n:generate
php artisan ziggy:generate resources/assets/scripts/helpers/ziggy.js
npm run build
```
The command above will download all of the dependencies necessary to get Pterodactyl assets building. After that, its as
simple as running the command below to generate assets while you're developing.
```bash
# build the compiled assets for development
yarn run build
# build the assets automatically when files are modified
yarn run watch
```
### Hot Module Reloading
For more advanced users, we also support 'Hot Module Reloading', allowing you to quickly see changes you're making
to the Vue template files without having to reload the page you're on. To Get started with this, you just need
to run the command below.
```bash
PUBLIC_PATH=http://192.168.1.1:8080 yarn run serve --host 192.168.1.1
```
There are two _very important_ parts of this command to take note of and change for your specific environment. The first
is the `--host` flag, which is required and should point to the machine where the `webpack-serve` server will be running.
The second is the `PUBLIC_PATH` environment variable which is the URL pointing to the HMR server and is appended to all of
the asset URLs used in Pterodactyl.
#### Vagrant
If you want to use HMR with our Vagrant image, you can use `yarn run v:serve` as a shortcut for the correct parameters.
In order to have proper file change detection you can use the [`vagrant-notify-forwarder`](https://github.com/mhallin/vagrant-notify-forwarder) to notify file events from the host to the VM.
```sh
vagrant plugin install vagrant-notify-forwarder
vagrant reload
```
### Building for Production
Once you have your files squared away and ready for the live server, you'll be needing to generate compiled, minified, and
hashed assets to push live. To do so, run the command below:
```bash
yarn run build:production
```
This will generate a production ready `bundle.js` and `bundle.css` as well as a `manifest.json` and store them in
the `/public/assets` directory where they can then be access by clients, and read by the Panel.

View file

@ -0,0 +1,16 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Client;
use Illuminate\Http\Request;
use Pterodactyl\Transformers\Api\Client\AccountTransformer;
class AccountController extends ClientApiController
{
public function index(Request $request): array
{
return $this->fractal->item($request->user())
->transformWith($this->getTransformer(AccountTransformer::class))
->toArray();
}
}

View file

@ -16,6 +16,7 @@ use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Pterodactyl\Traits\Helpers\ProvidesJWTServices;
use Pterodactyl\Transformers\Api\Client\AccountTransformer;
use Illuminate\Contracts\Cache\Repository as CacheRepository;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
@ -137,27 +138,37 @@ abstract class AbstractLoginController extends Controller
$request->session()->regenerate();
$this->clearLoginAttempts($request);
$token = $this->builder->setIssuer(config('app.url'))
->setAudience(config('app.url'))
->setId(str_random(12), true)
->setIssuedAt(Chronos::now()->getTimestamp())
->setNotBefore(Chronos::now()->getTimestamp())
->setExpiration(Chronos::now()->addSeconds(config('session.lifetime'))->getTimestamp())
->set('user', $user->only([
'id', 'uuid', 'username', 'email', 'name_first', 'name_last', 'language', 'root_admin',
]))
->sign($this->getJWTSigner(), $this->getJWTSigningKey())
->getToken();
$this->auth->guard()->login($user, true);
return response()->json([
'complete' => true,
'intended' => $this->redirectPath(),
'token' => $token->__toString(),
'jwt' => $this->createJsonWebToken($user),
]);
}
/**
* Create a new JWT for the request and sign it using the signing key.
*
* @param User $user
* @return string
*/
protected function createJsonWebToken(User $user): string
{
$token = $this->builder
->setIssuer('Pterodactyl Panel')
->setAudience(config('app.url'))
->setId(str_random(16), true)
->setIssuedAt(Chronos::now()->getTimestamp())
->setNotBefore(Chronos::now()->getTimestamp())
->setExpiration(Chronos::now()->addSeconds(config('session.lifetime'))->getTimestamp())
->set('user', (new AccountTransformer())->transform($user))
->sign($this->getJWTSigner(), $this->getJWTSigningKey())
->getToken();
return $token->__toString();
}
/**
* Determine if the user is logging in using an email or username,.
*

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Http;
use Pterodactyl\Http\Middleware\MaintenanceMiddleware;
use Pterodactyl\Models\ApiKey;
use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Auth\Middleware\Authenticate;
@ -21,6 +20,7 @@ use Illuminate\Routing\Middleware\SubstituteBindings;
use Pterodactyl\Http\Middleware\AccessingValidServer;
use Pterodactyl\Http\Middleware\Api\SetSessionDriver;
use Illuminate\View\Middleware\ShareErrorsFromSession;
use Pterodactyl\Http\Middleware\MaintenanceMiddleware;
use Pterodactyl\Http\Middleware\RedirectIfAuthenticated;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Pterodactyl\Http\Middleware\Api\AuthenticateIPAccess;
@ -71,7 +71,7 @@ class Kernel extends HttpKernel
RequireTwoFactorAuthentication::class,
],
'api' => [
'throttle:120,1',
'throttle:240,1',
ApiSubstituteBindings::class,
SetSessionDriver::class,
'api..key:' . ApiKey::TYPE_APPLICATION,
@ -79,7 +79,7 @@ class Kernel extends HttpKernel
AuthenticateIPAccess::class,
],
'client-api' => [
'throttle:60,1',
'throttle:240,1',
SubstituteClientApiBindings::class,
SetSessionDriver::class,
'api..key:' . ApiKey::TYPE_ACCOUNT,

View file

@ -97,6 +97,16 @@ class AuthenticateKey
throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']);
}
// Run through the token validation and throw an exception if the token is not valid.
if (
$token->getClaim('nbf') > Chronos::now()->getTimestamp()
|| $token->getClaim('iss') !== 'Pterodactyl Panel'
|| $token->getClaim('aud') !== config('app.url')
|| $token->getClaim('exp') <= Chronos::now()->getTimestamp()
) {
throw new AccessDeniedHttpException;
}
return (new ApiKey)->forceFill([
'user_id' => object_get($token->getClaim('user'), 'id', 0),
'key_type' => ApiKey::TYPE_ACCOUNT,

View file

@ -58,8 +58,25 @@ class AssetHashService
public function url(string $resource): string
{
$file = last(explode('/', $resource));
$data = array_get($this->manifest(), $file, $file);
return '/' . ltrim(str_replace($file, array_get($this->manifest(), $file, $file), $resource), '/');
return str_replace($file, array_get($data, 'src', $file), $resource);
}
/**
* Return the data integrity hash for a resource.
*
* @param string $resource
* @return string
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function integrity(string $resource): string
{
$file = last(explode('/', $resource));
$data = array_get($this->manifest(), $file, $file);
return array_get($data, 'integrity', '');
}
/**
@ -72,7 +89,11 @@ class AssetHashService
*/
public function css(string $resource): string
{
return '<link href="' . $this->url($resource) . '" rel="stylesheet preload" crossorigin="anonymous" referrerpolicy="no-referrer">';
return '<link href="' . $this->url($resource) . '"
rel="stylesheet preload"
crossorigin="anonymous"
integrity="' . $this->integrity($resource) . '"
referrerpolicy="no-referrer">';
}
/**
@ -85,7 +106,9 @@ class AssetHashService
*/
public function js(string $resource): string
{
return '<script src="' . $this->url($resource) . '" crossorigin="anonymous"></script>';
return '<script src="' . $this->url($resource) . '"
integrity="' . $this->integrity($resource) . '"
crossorigin="anonymous"></script>';
}
/**

View file

@ -0,0 +1,37 @@
<?php
namespace Pterodactyl\Transformers\Api\Client;
use Pterodactyl\Models\User;
class AccountTransformer extends BaseClientTransformer
{
/**
* Return the resource name for the JSONAPI output.
*
* @return string
*/
public function getResourceName(): string
{
return 'user';
}
/**
* Return basic information about the currently logged in user.
*
* @param \Pterodactyl\Models\User $model
* @return array
*/
public function transform(User $model)
{
return [
'id' => $model->id,
'admin' => $model->root_admin,
'username' => $model->username,
'email' => $model->email,
'first_name' => $model->name_first,
'last_name' => $model->name_last,
'language' => $model->language,
];
}
}

View file

@ -1,123 +0,0 @@
const babel = require('gulp-babel');
const concat = require('gulp-concat');
const cssmin = require('gulp-cssmin');
const del = require('del');
const exec = require('child_process').exec;
const gulp = require('gulp');
const gulpif = require('gulp-if');
const postcss = require('gulp-postcss');
const rev = require('gulp-rev');
const uglify = require('gulp-uglify-es').default;
const webpackStream = require('webpack-stream');
const webpackConfig = require('./webpack.config.js');
const sourcemaps = require('gulp-sourcemaps');
const through = require('through2');
const argv = require('yargs')
.default('production', false)
.argv;
const paths = {
manifest: './public/assets',
assets: './public/assets/{css,scripts}/*.{css,js,map}',
styles: {
src: './resources/assets/styles/main.css',
dest: './public/assets/css',
},
scripts: {
src: './resources/assets/scripts/**/*.{js,vue}',
watch: ['./resources/assets/scripts/**/*.{js,vue}', './resources/lang/locales.js'],
dest: './public/assets/scripts',
},
};
/**
* Build un-compiled CSS into a minified version.
*/
function styles() {
return gulp.src(paths.styles.src)
.pipe(sourcemaps.init())
.pipe(postcss([
require('postcss-import'),
require('tailwindcss')('./tailwind.js'),
require('precss'),
require('postcss-preset-env')({stage: 0}),
require('autoprefixer'),
]))
.pipe(gulpif(argv.production, cssmin()))
.pipe(concat('bundle.css'))
.pipe(rev())
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(paths.styles.dest))
.pipe(rev.manifest(paths.manifest + '/manifest.json', {merge: true, base: paths.manifest}))
.pipe(gulp.dest(paths.manifest));
}
/**
* Build all of the waiting scripts.
*/
function scripts() {
return webpackStream(webpackConfig)
.pipe(gulpif(argv.production, uglify()))
.pipe(rev())
.pipe(gulp.dest(paths.scripts.dest))
.pipe(rev.manifest(paths.manifest + '/manifest.json', {merge: true, base: paths.manifest}))
.pipe(gulp.dest(paths.manifest));
}
/**
* Provides watchers.
*/
function watch() {
gulp.watch(['./resources/assets/styles/**/*.css'], gulp.series(function cleanStyles() {
return del(['./public/assets/css/**/*.{css,map}']);
}, styles));
gulp.watch(paths.scripts.watch, gulp.series(function cleanScripts() {
return del(['./public/assets/scripts/**/*.{js,map}']);
}, scripts));
}
/**
* Generate the language files to be consumed by front end.
*
* @returns {Promise<any>}
*/
function i18n() {
return new Promise((resolve, reject) => {
exec('php artisan vue-i18n:generate', {}, (err, stdout, stderr) => {
return err ? reject(err) : resolve({ stdout, stderr });
})
})
}
/**
* Generate the routes file to be used in Vue files.
*
* @returns {Promise<any>}
*/
function routes() {
return new Promise((resolve, reject) => {
exec('php artisan ziggy:generate resources/assets/scripts/helpers/ziggy.js', {}, (err, stdout, stderr) => {
return err ? reject(err) : resolve({ stdout, stderr });
});
})
}
/**
* Cleanup unused versions of hashed assets.
*/
function clean() {
return del([paths.assets]);
}
exports.clean = clean;
exports.i18n = i18n;
exports.routes = routes;
exports.styles = styles;
exports.scripts = scripts;
exports.watch = watch;
gulp.task('components', gulp.parallel(i18n, routes));
gulp.task('scripts', gulp.series(clean, scripts));
gulp.task('default', gulp.series(clean, i18n, routes, styles, scripts));

13718
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,19 @@
{
"name": "pterodactyl-panel",
"dependencies": {
"vue": "^2.5.7",
"vue-axios": "^2.1.1",
"vue-router": "^3.0.1",
"vuex": "^3.0.1",
"vuex-i18n": "^1.10.5",
"vuex-router-sync": "^5.0.0"
},
"devDependencies": {
"@babel/core": "^7.0.0-beta.49",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0-beta.49",
"@babel/plugin-transform-async-to-generator": "^7.0.0-beta.49",
"@babel/plugin-transform-runtime": "^7.0.0-beta.49",
"@babel/preset-env": "^7.0.0-beta.49",
"@fortawesome/fontawesome": "^1.1.8",
"@fortawesome/fontawesome-free-solid": "^5.0.13",
"@fortawesome/vue-fontawesome": "0.0.22",
"autoprefixer": "^8.2.0",
"axios": "^0.18.0",
"babel-cli": "6.18.0",
@ -17,59 +22,46 @@
"babel-plugin-transform-object-assign": "^6.22.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-plugin-transform-strict-mode": "^6.18.0",
"babel-preset-es2015": "^6.24.1",
"babel-register": "^6.26.0",
"clean-webpack-plugin": "^0.1.19",
"css-loader": "^0.28.11",
"del": "^3.0.0",
"gulp": "^4.0.0",
"gulp-babel": "^7.0.1",
"gulp-cli": "^2.0.1",
"gulp-concat": "^2.6.1",
"gulp-cssmin": "^0.2.0",
"gulp-if": "^2.0.2",
"gulp-postcss": "^7.0.1",
"gulp-rename": "^1.2.2",
"gulp-rev": "^8.1.1",
"gulp-sourcemaps": "^2.6.4",
"gulp-uglify-es": "^1.0.1",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"glob-all": "^3.1.0",
"html-webpack-plugin": "^3.2.0",
"jquery": "^3.3.1",
"jwt-decode": "^2.2.0",
"lodash": "^4.17.5",
"luxon": "^1.2.1",
"postcss": "^6.0.21",
"postcss-import": "^11.1.0",
"postcss-loader": "^2.1.5",
"postcss-preset-env": "^3.4.0",
"postcss-scss": "^1.0.4",
"precss": "^3.1.2",
"pug-plain-loader": "^1.0.0",
"purgecss-webpack-plugin": "^1.1.0",
"style-loader": "^0.21.0",
"tailwindcss": "^0.5.1",
"through2": "^2.0.3",
"vee-validate": "^2.0.9",
"vue": "^2.5.7",
"vue-axios": "^2.1.1",
"uglifyjs-webpack-plugin": "^1.2.5",
"vue-devtools": "^3.1.9",
"vue-feather-icons": "^4.7.1",
"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",
"vuex": "^3.0.1",
"vuex-i18n": "^1.10.5",
"webpack": "^4.4.1",
"webpack-stream": "^4.0.3",
"xterm": "^3.4.1",
"yargs": "^11.0.0"
"webpack-assets-manifest": "^3.0.1",
"webpack-cli": "^3.0.2",
"webpack-hot-client": "^4.0.2",
"webpack-manifest-plugin": "^2.0.3",
"webpack-serve": "^1.0.2",
"webpack-shell-plugin": "^0.5.0",
"webpack-stream": "^4.0.3"
},
"scripts": {
"build:filemanager": "./node_modules/babel-cli/bin/babel.js public/themes/pterodactyl/js/frontend/files/src --source-maps --out-file public/themes/pterodactyl/js/frontend/files/filemanager.min.js",
"watch": "./node_modules/gulp-cli/bin/gulp.js watch",
"build": "./node_modules/gulp-cli/bin/gulp.js default",
"build:components": "./node_modules/gulp-cli/bin/gulp.js components",
"build:styles": "./node_modules/gulp-cli/bin/gulp.js styles",
"build:scripts": "./node_modules/gulp-cli/bin/gulp.js scripts"
},
"dependencies": {
"vuex-router-sync": "^5.0.0"
"watch": "NODE_ENV=development ./node_modules/.bin/webpack --watch --progress",
"build": "NODE_ENV=development ./node_modules/.bin/webpack --progress",
"build:production": "NODE_ENV=production ./node_modules/.bin/webpack",
"serve": "webpack-serve --hot --config ./webpack.config.js",
"v:serve": "PUBLIC_PATH=http://192.168.50.2:8080 NODE_ENV=development webpack-serve --hot --config ./webpack.config.js --host 192.168.50.2 --no-clipboard"
}
}

View file

@ -0,0 +1,18 @@
<html>
<head>
<title>Pterodactyl Dev</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
</head>
<body>
<div id="pterodactyl">
<router-view></router-view>
<div class="w-full m-auto mt-0 container">
<p class="text-right text-grey-dark text-xs">
</p>
</div>
</div>
</body>
</html>

View file

@ -3,16 +3,13 @@ import Vuex from 'vuex';
import vuexI18n from 'vuex-i18n';
import VueRouter from 'vue-router';
require('./bootstrap');
// Helpers
import { Ziggy } from './helpers/ziggy';
import Locales from './../../../resources/lang/locales';
import { flash } from './mixins/flash';
import fontawesome from '@fortawesome/fontawesome';
import faSolid from '@fortawesome/fontawesome-free-solid';
import FontAwesomeIcon from '@fortawesome/vue-fontawesome';
fontawesome.library.add(faSolid);
import { routes } from './routes';
import createStore from './store';
@ -27,9 +24,10 @@ const router = new VueRouter({
Vue.use(Vuex);
const store = createStore(router);
Vue.config.productionTip = false;
const route = require('./../../../vendor/tightenco/ziggy/src/js/route').default;
Vue.config.productionTip = false;
Vue.mixin({ methods: { route } });
Vue.mixin(flash);
@ -38,9 +36,8 @@ Vue.use(vuexI18n.plugin, store);
Vue.i18n.add('en', Locales.en);
Vue.i18n.set('en');
Vue.component('font-awesome-icon', FontAwesomeIcon);
require('./bootstrap');
if (module.hot) {
module.hot.accept();
}
const app = new Vue({ store, router }).$mount('#pterodactyl');

View file

@ -1,3 +1,5 @@
import axios from './helpers/axios';
window._ = require('lodash');
/**
@ -10,24 +12,7 @@ try {
window.$ = window.jQuery = require('jquery');
} catch (e) {}
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
window.axios.defaults.headers.common['Accept'] = 'application/json';
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;
});
}
window.axios = axios;
/**
* Next we will register the CSRF Token as a common header with Axios so that

View file

@ -77,32 +77,21 @@
this.$data.showSpinner = true;
this.clearFlashes();
axios.post(this.route('auth.login'), {
user: this.$props.user.email,
password: this.$props.user.password,
})
.then(function (response) {
// If there is a 302 redirect or some other odd behavior (basically, response that isnt
// in JSON format) throw an error and don't try to continue with the login.
if (!(response.data instanceof Object)) {
throw new Error('An error was encountered while processing this request.');
this.$store.dispatch('auth/login', { user: this.$props.user.email, password: this.$props.user.password })
.then(response => {
if (response.complete) {
return window.location = response.intended;
}
if (response.data.complete) {
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.login_token}});
this.$props.user.password = '';
this.$data.showSpinner = false;
this.$router.push({name: 'checkpoint', query: {token: response.login_token}});
})
.catch(function (err) {
self.$props.user.password = '';
self.$data.showSpinner = false;
self.$refs.password.focus();
self.$store.dispatch('logout');
.catch(err => {
this.$props.user.password = '';
this.$data.showSpinner = false;
this.$refs.password.focus();
this.$store.dispatch('auth/logout');
if (!err.response) {
return console.error(err);

View file

@ -29,6 +29,7 @@
<script>
import { DateTime } from 'luxon';
import Server from '../../models/server';
import _ from 'lodash';
import Flash from '../Flash';
import ServerBox from './ServerBox';
@ -75,7 +76,6 @@
},
methods: {
/**
* Handle a search for servers but only call the search function every 500ms
* at the fastest.

View file

@ -1,6 +1,6 @@
<template>
<div class="server-box animate fadein">
<router-link :to="{ name: 'server', params: { id: server.identifier }}" class="content">
<router-link :to="{ name: 'server', params: { serverID: server.identifier }}" class="content">
<div class="float-right">
<div class="indicator" :class="status"></div>
</div>

View file

@ -38,32 +38,32 @@
</div>
</div>
<div class="sidenav">
<router-link :to="{ name: 'server' }">
<font-awesome-icon class="mr-2" fixed-with icon="terminal"/>
<router-link :to="{ name: 'server', params: { serverID: this.$route.params.serverID } }">
<terminal-icon style="height: 1em;"></terminal-icon>
Console
</router-link>
<router-link :to="{ name: 'server-files' }">
<font-awesome-icon class="mr-2" fixed-with icon="folder-open"/>
<folder-icon style="height: 1em;"></folder-icon>
Files
</router-link>
<router-link :to="{ name: 'server-subusers' }">
<font-awesome-icon class="mr-2" fixed-with icon="users"/>
<users-icon style="height: 1em;"></users-icon>
Subusers
</router-link>
<router-link :to="{ name: 'server-schedules' }">
<font-awesome-icon class="mr-2" fixed-with icon="calendar-alt"/>
<calendar-icon style="height: 1em;"></calendar-icon>
Schedules
</router-link>
<router-link :to="{ name: 'server-databases' }">
<font-awesome-icon class="mr-2" fixed-with icon="database"/>
<database-icon style="height: 1em;"></database-icon>
Databases
</router-link>
<router-link :to="{ name: 'server-allocations' }">
<font-awesome-icon class="mr-2" fixed-with icon="globe"/>
<globe-icon style="height: 1em;"></globe-icon>
Allocations
</router-link>
<router-link :to="{ name: 'server-settings' }">
<font-awesome-icon class="mr-2" fixed-with icon="cog"/>
<settings-icon style="height: 1em;"></settings-icon>
Settings
</router-link>
</div>
@ -77,10 +77,14 @@
</template>
<script>
import { TerminalIcon, FolderIcon, UsersIcon, CalendarIcon, DatabaseIcon, GlobeIcon, SettingsIcon } from 'vue-feather-icons'
import ServerConsole from "./ServerConsole";
import Navigation from '../core/Navigation';
export default {
components: {Navigation, ServerConsole}
components: {
Navigation, ServerConsole, TerminalIcon, FolderIcon, UsersIcon,
CalendarIcon, DatabaseIcon, GlobeIcon, SettingsIcon
}
}
</script>

View file

@ -0,0 +1 @@
ziggy.js

View file

@ -0,0 +1,22 @@
import User from './../models/user';
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
let axios = require('axios');
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
axios.defaults.headers.common['Accept'] = 'application/json';
axios.defaults.headers.common['Authorization'] = `Bearer ${User.getToken()}`;
if (typeof phpdebugbar !== 'undefined') {
axios.interceptors.response.use(function (response) {
phpdebugbar.ajaxHandler.handle(response.request);
return response;
});
}
export default axios;

File diff suppressed because one or more lines are too long

View file

@ -1,114 +1,21 @@
import { Collection, Model } from 'vue-mc';
/**
* A generic server model used throughout the code base.
*/
export class Server extends Model {
/**
* Identifier the primary identifier for this model.
*
* @returns {{identifier: string}}
*/
static options() {
return {
identifier: 'identifier',
};
}
/**
* Return the defaults for this model.
*
* @returns {object}
*/
static defaults() {
return {
uuid: null,
identifier: null,
name: '',
description: '',
node: '',
limits: {
memory: 0,
swap: 0,
disk: 0,
io: 0,
cpu: 0,
},
allocation: {
ip: null,
port: null,
},
feature_limits: {
databases: 0,
allocations: 0,
},
};
}
/**
* Mutations to apply to items in this model.
*
* @returns {{name: StringConstructor, description: StringConstructor}}
*/
static mutations() {
return {
uuid: String,
identifier: String,
name: String,
description: String,
node: String,
limits: {
memory: Number,
swap: Number,
disk: Number,
io: Number,
cpu: Number,
},
allocation: {
ip: String,
port: Number,
},
feature_limits: {
databases: Number,
allocations: Number,
}
};
}
/**
* Routes to use when building models.
*
* @returns {{fetch: string}}
*/
static routes() {
return {
fetch: '/api/client/servers/{identifier}',
};
}
}
export class ServerCollection extends Collection {
static model() {
return Server;
}
static defaults() {
return {
orderBy: identifier,
};
}
static routes() {
return {
fetch: '/api/client',
};
}
get todo() {
return this.sum('done');
}
get done() {
return this.todo === 0;
export default class Server {
constructor({
identifier,
uuid,
name,
node,
description,
allocation,
limits,
feature_limits
}) {
this.identifier = identifier;
this.uuid = uuid;
this.name = name;
this.node = node;
this.description = description;
this.allocation = allocation;
this.limits = limits;
this.feature_limits = feature_limits;
}
}

View file

@ -1,48 +1,63 @@
import { Collection, Model } from 'vue-mc';
import JwtDecode from 'jwt-decode';
import isString from 'lodash/isString';
import jwtDecode from 'jwt-decode';
export class User extends Model {
static defaults() {
return {
id: null,
uuid: '',
username: '',
email: '',
name_first: '',
name_last: '',
language: 'en',
root_admin: false,
export default class User {
/**
* Get a new user model from the JWT.
*
* @return {User | null}
*/
static fromToken(token) {
if (!isString(token)) {
token = localStorage.getItem('token');
}
if (!isString(token) || token.length < 1) {
return null;
}
const data = jwtDecode(token);
if (data.user) {
return new User(data.user);
}
return null;
}
static mutations() {
return {
id: Number,
uuid: String,
username: String,
email: String,
name_first: String,
name_last: String,
language: String,
root_admin: Boolean,
}
/**
* Return the JWT for the authenticated user.
*
* @returns {string | null}
*/
static getToken()
{
return localStorage.getItem('token');
}
static fromJWT(token) {
return new User(JwtDecode(token).user || {});
}
}
export class UserCollection extends Collection {
static model() {
return User;
}
get todo() {
return this.sum('done');
}
get done() {
return this.todo === 0;
/**
* Create a new user model.
*
* @param {Boolean} admin
* @param {String} username
* @param {String} email
* @param {String} first_name
* @param {String} last_name
* @param {String} language
*/
constructor({
admin,
username,
email,
first_name,
last_name,
language,
}) {
this.admin = admin;
this.username = username;
this.email = email;
this.name = `${first_name} ${last_name}`;
this.first_name = first_name;
this.last_name = last_name;
this.language = language;
}
}

View file

@ -1,14 +1,16 @@
import Vuex from 'vuex';
import { sync } from 'vuex-router-sync';
import { serverModule } from "./modules/server";
import { userModule } from './modules/user'
import { userModule } from './modules/user';
import { authModule } from "./modules/auth";
const createStore = (router) => {
const store = new Vuex.Store({
//strict: process.env.NODE_ENV !== 'production',
strict: process.env.NODE_ENV !== 'production',
modules: {
userModule,
serverModule,
authModule,
},
});
sync(store, router);

View file

@ -0,0 +1,71 @@
import User from './../../models/user';
const route = require('./../../../../../vendor/tightenco/ziggy/src/js/route').default;
export const authModule = {
namespaced: true,
state: {
user: User.fromToken(),
},
getters: {
/**
* Return the currently authenticated user.
*
* @param state
* @returns {User|null}
*/
currentUser: function (state) {
return state.user;
}
},
setters: {},
actions: {
login: ({commit}, {user, password}) => {
return new Promise((resolve, reject) => {
window.axios.post(route('auth.login'), {user, password})
.then(response => {
commit('logout');
// If there is a 302 redirect or some other odd behavior (basically, response that isnt
// in JSON format) throw an error and don't try to continue with the login.
if (!(response.data instanceof Object)) {
return reject(new Error('An error was encountered while processing this request.'));
}
if (response.data.complete) {
commit('login', {jwt: response.data.jwt});
return resolve({
complete: true,
intended: response.data.intended,
});
}
return resolve({
complete: false,
token: response.data.login_token,
});
})
.catch(reject);
});
},
logout: function ({commit}) {
return new Promise((resolve, reject) => {
window.axios.get(route('auth.logout'))
.then(() => {
commit('logout');
return resolve();
})
.catch(reject);
})
},
},
mutations: {
login: function (state, {jwt}) {
localStorage.setItem('token', jwt);
state.user = User.fromToken(jwt);
},
logout: function (state) {
localStorage.removeItem('token');
state.user = null;
},
},
};

View file

@ -19,7 +19,7 @@
@show
@section('assets')
{!! $asset->css('assets/css/bundle.css') !!}
{!! $asset->css('main.css') !!}
@show
@include('layouts.scripts')
@ -33,7 +33,7 @@
@yield('below-container')
@show
@section('scripts')
{!! $asset->js('assets/scripts/app.js') !!}
{!! $asset->js('main.js') !!}
@show
</body>
</html>

View file

@ -12,6 +12,10 @@ use Pterodactyl\Http\Middleware\Api\Client\AuthenticateClientAccess;
*/
Route::get('/', 'ClientController@index')->name('api.client.index');
Route::group(['prefix' => '/account'], function () {
Route::get('/', 'AccountController@index')->name('api.client.account');
});
/*
|--------------------------------------------------------------------------
| Client Control API

View file

@ -1,42 +1,145 @@
const _ = require('lodash');
const path = require('path');
const tailwind = require('tailwindcss');
const glob = require('glob-all');
const AssetsManifestPlugin = require('webpack-assets-manifest');
const CleanPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const ShellPlugin = require('webpack-shell-plugin');
const PurgeCssPlugin = require('purgecss-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
// Custom PurgeCSS extractor for Tailwind that allows special characters in
// class names.
//
// https://github.com/FullHuman/purgecss#extractor
class TailwindExtractor {
static extract (content) {
return content.match(/[A-z0-9-:\/]+/g) || [];
}
}
const basePlugins = [
new CleanPlugin(path.resolve(__dirname, 'public/assets')),
new ShellPlugin({
onBuildStart: [
'php artisan vue-i18n:generate',
'php artisan ziggy:generate resources/assets/scripts/helpers/ziggy.js',
],
}),
new ExtractTextPlugin('bundle-[hash].css', {
allChunks: true,
}),
new AssetsManifestPlugin({
writeToDisk: true,
publicPath: true,
integrity: true,
integrityHashes: ['sha384'],
}),
];
const productionPlugins = [
new PurgeCssPlugin({
paths: glob.sync([
path.join(__dirname, 'resources/assets/scripts/**/*.vue'),
path.join(__dirname, 'resources/themes/pterodactyl/**/*.blade.php'),
]),
extractors: [
{
extractor: TailwindExtractor,
extensions: ['html', 'js', 'php', 'vue'],
}
],
}),
new UglifyJsPlugin({
include: [
path.join(__dirname, 'resources/assets/scripts'),
path.join(__dirname, 'node_modules'),
path.join(__dirname, 'vendor/tightenco'),
],
cache: true,
parallel: 2,
}),
];
module.exports = {
entry: './resources/assets/scripts/app.js',
mode: process.env.NODE_ENV,
devtool: process.env.NODE_ENV === 'production' ? false : 'source-map',
performance: {
hints: false,
},
// Passing an array loads them all but only exports the last.
entry: ['./resources/assets/styles/main.css', './resources/assets/scripts/app.js'],
output: {
filename: 'app.js',
path: path.resolve(__dirname, 'public/assets'),
filename: 'bundle-[hash].js',
publicPath: _.get(process.env, 'PUBLIC_PATH', '') + '/assets/',
crossOriginLoading: 'anonymous',
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
postcss: [
require('postcss-import'),
require('postcss-preset-env')({stage: 0}),
require('tailwindcss')('./tailwind.js'),
require('autoprefixer'),
]
}
},
{
test: /\.js$/,
exclude: /(node_modules|vendor)/,
use: [{
loader: "babel-loader"
}]
include: [
path.resolve(__dirname, 'resources'),
],
loader: 'babel-loader?cacheDirectory',
},
{
test: /\.pug$/,
loader: 'pug-plain-loader'
},
test: /\.css$/,
include: [
path.resolve(__dirname, 'resources'),
],
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [{
loader: 'css-loader',
options: {
sourceMap: true,
importLoaders: 1,
},
}, {
loader: 'postcss-loader',
options: {
ident: 'postcss',
sourceMap: true,
plugins: [
require('postcss-import'),
tailwind('./tailwind.js'),
require('postcss-preset-env')({stage: 0}),
require('precss'),
require('autoprefixer'),
require('cssnano'),
]
},
}],
}),
}
]
},
resolve: {
alias: {
// 'vue': 'vue/dist/vue.js'
'vue$': 'vue/dist/vue.esm.js'
},
extensions: ['*', '.js', '.vue', '.json']
extensions: ['.js', '.vue', '.json'],
symlinks: false,
},
plugins: [],
devtool: 'source-map',
plugins: process.env.NODE_ENV === 'production' ? basePlugins.concat(productionPlugins) : basePlugins,
serve: {
content: "./public/",
dev: {
publicPath: "/assets/",
headers: {
"Access-Control-Allow-Origin": "*",
},
},
hot: {
hmr: true,
reload: true,
}
}
};

4509
yarn.lock

File diff suppressed because it is too large Load diff