Merge branch 'feature/vuejs' into feature/vue-serverview
This commit is contained in:
commit
05478e3277
29 changed files with 2997 additions and 16217 deletions
|
@ -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
|
||||
|
||||
|
|
61
BUILDING.md
61
BUILDING.md
|
@ -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.
|
||||
|
|
16
app/Http/Controllers/Api/Client/AccountController.php
Normal file
16
app/Http/Controllers/Api/Client/AccountController.php
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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,.
|
||||
*
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
37
app/Transformers/Api/Client/AccountTransformer.php
Normal file
37
app/Transformers/Api/Client/AccountTransformer.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
123
gulpfile.js
123
gulpfile.js
|
@ -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
13718
package-lock.json
generated
File diff suppressed because it is too large
Load diff
64
package.json
64
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
18
resources/assets/index.html
Normal file
18
resources/assets/index.html
Normal 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>
|
|
@ -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');
|
||||
|
|
21
resources/assets/scripts/bootstrap.js
vendored
21
resources/assets/scripts/bootstrap.js
vendored
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
1
resources/assets/scripts/helpers/.gitignore
vendored
Normal file
1
resources/assets/scripts/helpers/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
ziggy.js
|
22
resources/assets/scripts/helpers/axios.js
Normal file
22
resources/assets/scripts/helpers/axios.js
Normal 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
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
static mutations() {
|
||||
return {
|
||||
id: Number,
|
||||
uuid: String,
|
||||
username: String,
|
||||
email: String,
|
||||
name_first: String,
|
||||
name_last: String,
|
||||
language: String,
|
||||
root_admin: Boolean,
|
||||
}
|
||||
if (!isString(token) || token.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
const data = jwtDecode(token);
|
||||
if (data.user) {
|
||||
return new User(data.user);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the JWT for the authenticated user.
|
||||
*
|
||||
* @returns {string | null}
|
||||
*/
|
||||
static getToken()
|
||||
{
|
||||
return localStorage.getItem('token');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
71
resources/assets/scripts/store/modules/auth.js
Normal file
71
resources/assets/scripts/store/modules/auth.js
Normal 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;
|
||||
},
|
||||
},
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue