1
1
mirror of https://github.com/pterodactyl/panel.git synced 2024-11-26 11:02:31 +01:00

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

This commit is contained in:
Jakob Schrettenbrunner 2018-05-29 00:04:41 +02:00
commit 378a1859cf
21 changed files with 497 additions and 68 deletions

View File

@ -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))

View File

@ -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(),
]);
}
}

View File

@ -3,6 +3,7 @@
namespace Pterodactyl\Http\Middleware\Api;
use Closure;
use Lcobucci\JWT\Parser;
use Cake\Chronos\Chronos;
use Illuminate\Http\Request;
use Pterodactyl\Models\ApiKey;
@ -63,6 +64,23 @@ class AuthenticateKey
}
$raw = $request->bearerToken();
// This is an internal JWT, treat it differently to get the correct user
// before passing it along.
if (strlen($raw) > ApiKey::IDENTIFIER_LENGTH + ApiKey::KEY_LENGTH) {
$token = (new Parser)->parse($raw);
$model = (new ApiKey)->fill([
'user_id' => $token->getClaim('uid'),
'key_type' => ApiKey::TYPE_ACCOUNT,
]);
$this->auth->guard()->loginUsingId($token->getClaim('uid'));
$request->attributes->set('api_key', $model);
return $next($request);
}
$identifier = substr($raw, 0, ApiKey::IDENTIFIER_LENGTH);
$token = substr($raw, ApiKey::IDENTIFIER_LENGTH);

View File

@ -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);

View File

@ -28,7 +28,12 @@ class ServerTransformer extends BaseClientTransformer
'identifier' => $server->uuidShort,
'uuid' => $server->uuid,
'name' => $server->name,
'node' => $server->node->name,
'description' => $server->description,
'allocation' => [
'ip' => $server->allocation->alias,
'port' => $server->allocation->port,
],
'limits' => [
'memory' => $server->memory,
'swap' => $server->swap,

View File

@ -26,6 +26,7 @@
"laracasts/utilities": "^3.0",
"laravel/framework": "5.6.*",
"laravel/tinker": "^1.0",
"lcobucci/jwt": "^3.2",
"lord/laroute": "^2.4",
"matriphe/iso-639": "^1.2",
"nesbot/carbon": "^1.22",

60
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "9232ff40da15c9430731254edc662eb7",
"content-hash": "f84af54d009a128472ca7e19a50fccf8",
"packages": [
{
"name": "appstract/laravel-blade-directives",
@ -1569,6 +1569,64 @@
],
"time": "2018-05-17T13:42:07+00:00"
},
{
"name": "lcobucci/jwt",
"version": "3.2.2",
"source": {
"type": "git",
"url": "https://github.com/lcobucci/jwt.git",
"reference": "0b5930be73582369e10c4d4bb7a12bac927a203c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lcobucci/jwt/zipball/0b5930be73582369e10c4d4bb7a12bac927a203c",
"reference": "0b5930be73582369e10c4d4bb7a12bac927a203c",
"shasum": ""
},
"require": {
"ext-openssl": "*",
"php": ">=5.5"
},
"require-dev": {
"mdanter/ecc": "~0.3.1",
"mikey179/vfsstream": "~1.5",
"phpmd/phpmd": "~2.2",
"phpunit/php-invoker": "~1.1",
"phpunit/phpunit": "~4.5",
"squizlabs/php_codesniffer": "~2.3"
},
"suggest": {
"mdanter/ecc": "Required to use Elliptic Curves based algorithms."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.1-dev"
}
},
"autoload": {
"psr-4": {
"Lcobucci\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Luís Otávio Cobucci Oblonczyk",
"email": "lcobucci@gmail.com",
"role": "developer"
}
],
"description": "A simple library to work with JSON Web Token and JSON Web Signature",
"keywords": [
"JWS",
"jwt"
],
"time": "2017-09-01T08:23:26+00:00"
},
{
"name": "league/flysystem",
"version": "1.0.45",

49
package-lock.json generated
View File

@ -6122,6 +6122,12 @@
"integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=",
"dev": true
},
"jwt-decode": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz",
"integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=",
"dev": true
},
"kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
@ -7006,6 +7012,12 @@
"integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE=",
"dev": true
},
"moment": {
"version": "2.22.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz",
"integrity": "sha512-shJkRTSebXvsVqk56I+lkb2latjBs8I+pc2TzWc545y2iFnSjm7Wg0QMh+ZWcdSLQyGEau5jI8ocnmkyTgr9YQ==",
"dev": true
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -11075,12 +11087,24 @@
"spdx-expression-parse": "3.0.0"
}
},
"validator": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-8.2.0.tgz",
"integrity": "sha512-Yw5wW34fSv5spzTXNkokD6S6/Oq92d8q/t14TqsS3fAiA1RYnxSFSIZ+CY3n6PGGRCq5HhJTSepQvFUS2QUDxA==",
"dev": true
},
"value-or-function": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz",
"integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=",
"dev": true
},
"vee-validate": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/vee-validate/-/vee-validate-2.0.9.tgz",
"integrity": "sha512-0qA3hrpF2jIBoEReWF8YkvG1ukJVS56+oyPTxOtb2OfB5d7iUuQiyboOOpXOvOViREHNXTsIcQ5XIQOMBff/wg==",
"dev": true
},
"vendors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.2.tgz",
@ -11241,6 +11265,31 @@
}
}
},
"vue-mc": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/vue-mc/-/vue-mc-0.2.4.tgz",
"integrity": "sha1-k1actuCOLRxSlop0zOimssm9pmo=",
"dev": true,
"requires": {
"axios": "0.16.2",
"lodash": "4.17.5",
"moment": "2.22.1",
"validator": "8.2.0",
"vue": "2.5.16"
},
"dependencies": {
"axios": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.16.2.tgz",
"integrity": "sha1-uk+S8XFn37q0CYN4VFS5rBScPG0=",
"dev": true,
"requires": {
"follow-redirects": "1.5.0",
"is-buffer": "1.1.6"
}
}
}
},
"vue-router": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.0.1.tgz",

View File

@ -24,16 +24,19 @@
"gulp-rev": "^8.1.1",
"gulp-uglify-es": "^1.0.1",
"jquery": "^3.3.1",
"jwt-decode": "^2.2.0",
"lodash": "^4.17.5",
"postcss": "^6.0.21",
"postcss-import": "^11.1.0",
"postcss-preset-env": "^3.4.0",
"postcss-scss": "^1.0.4",
"tailwindcss": "^0.5.1",
"vee-validate": "^2.0.9",
"vue": "^2.5.7",
"vue-axios": "^2.1.1",
"vue-devtools": "^3.1.9",
"vue-loader": "^14.2.2",
"vue-mc": "^0.2.4",
"vue-router": "^3.0.1",
"vue-template-compiler": "^2.5.16",
"vueify-insert-css": "^1.0.0",

View File

@ -13,19 +13,14 @@ import faSolid from '@fortawesome/fontawesome-free-solid';
import FontAwesomeIcon from '@fortawesome/vue-fontawesome';
fontawesome.library.add(faSolid);
// Base Vuejs Templates
import Login from './components/auth/Login';
import Dashboard from './components/dashboard/Dashboard';
import Account from './components/dashboard/Account';
import ResetPassword from './components/auth/ResetPassword';
import { Server, ServerConsole, ServerAllocations, ServerDatabases, ServerFiles, ServerSchedules, ServerSettings, ServerSubusers } from './components/server';
import { routes } from './routes';
import { storeData } from './store';
window.events = new Vue;
window.Ziggy = Ziggy;
Vue.use(Vuex);
const store = new Vuex.Store();
const store = new Vuex.Store(storeData);
const route = require('./../../../vendor/tightenco/ziggy/src/js/route').default;
Vue.config.productionTip = false;
@ -41,35 +36,7 @@ Vue.i18n.set('en');
Vue.component('font-awesome-icon', FontAwesomeIcon);
const router = new VueRouter({
mode: 'history',
routes: [
{ name: 'login', path: '/auth/login', component: Login },
{ name: 'forgot-password', path: '/auth/password', component: Login },
{ name: 'checkpoint', path: '/checkpoint', component: Login },
{
name: 'reset-password',
path: '/auth/password/reset/:token',
component: ResetPassword,
props: function (route) {
return { token: route.params.token, email: route.query.email || '' };
}
},
{ name : 'index', path: '/', component: Dashboard },
{ name : 'account', path: '/account', component: Account },
{ name : 'account-api', path: '/account/api', component: Account },
{ name : 'account-security', path: '/account/security', component: Account },
{ path: '/server/:id', component: Server,
children: [
{ name: 'server', path: '', component: ServerConsole },
{ name: 'server-files', path: 'files', component: ServerFiles },
{ name: 'server-subusers', path: 'subusers', component: ServerSubusers },
{ name: 'server-schedules', path: 'schedules', component: ServerSchedules },
{ name: 'server-databases', path: 'databases', component: ServerDatabases },
{ name: 'server-allocations', path: 'allocations', component: ServerAllocations },
{ name: 'server-settings', path: 'settings', component: ServerSettings },
]
}
]
mode: 'history', routes
});
require('./bootstrap');

View File

@ -19,6 +19,15 @@ try {
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
window.axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.token || '';
if (typeof phpdebugbar !== 'undefined') {
window.axios.interceptors.response.use(function (response) {
phpdebugbar.ajaxHandler.handle(response.request);
return response;
});
}
/**
* Next we will register the CSRF Token as a common header with Axios so that

View File

@ -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);
}

View File

@ -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);
}

View File

@ -7,11 +7,19 @@
ref="search"
/>
</div>
<transition-group class="w-full m-auto mt-4 animate fadein sm:flex flex-wrap content-start">
<router-link class="server-box" :to="{name: 'server', params: { id: server.uuidShort }}" :key="index" v-for="(server, index) in servers">
<div class="content">
<transition-group class="w-full m-auto mt-4 animate fadein sm:flex flex-wrap content-start"><div class="server-box" :key="index" v-for="(server, index) in servers.models">
<router-link :to="{ name: 'server', params: { id: server.identifier }}" class="content">
<div class="float-right">
<div class="indicator online"></div>
<div class="indicator"></div>
</div>
<div class="mb-4">
<div class="text-black font-bold text-xl">
{{ server.name }}
</div>
</div>
<div class="mb-0 flex">
<div class="usage">
<div class="indicator-title">CPU</div>
</div>
<div class="mb-4">
<div class="text-black font-bold text-xl">{{ server.name }}</div>
@ -34,19 +42,21 @@
<span class="font-light text-sm">Mb</span>
</div>
</div>
</div>
<div class="flex items-center">
<div class="text-sm">
<p class="text-grey">{{ server.node_name }}</p>
<p class="text-grey">{{ server.node }}</p>
<p class="text-grey-dark">{{ server.allocation.ip }}:{{ server.allocation.port }}</p>
</div>
</div>
</div>
</router-link>
</div>
</transition-group>
</div>
</template>
<script>
import { ServerCollection } from '../../models/server';
import _ from 'lodash';
export default {
@ -54,7 +64,7 @@
data: function () {
return {
search: '',
servers: [],
servers: new ServerCollection,
}
},
@ -69,15 +79,16 @@
* @param {string} query
*/
loadServers: function (query = '') {
const self = this;
window.axios.get(this.route('dashboard.servers'), {
window.axios.get(this.route('api.client.index'), {
params: { query },
})
.then(function (response) {
self.servers = response.data;
.then(response => {
this.servers = new ServerCollection;
response.data.data.forEach(obj => {
this.servers.add(obj.attributes);
});
})
.catch(function (error) {
.catch(error => {
console.error(error);
});
},

View File

@ -0,0 +1,19 @@
const Allocation = function () {
this.ip = null;
this.port = null;
};
/**
* Return a new allocation model.
*
* @param obj
* @returns {Allocation}
*/
Allocation.prototype.fill = function (obj) {
this.ip = obj.ip || null;
this.port = obj.port || null;
return this;
};
export default Allocation;

View File

@ -0,0 +1,114 @@
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;
}
}

View File

@ -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;

View File

@ -0,0 +1,46 @@
// Base Vuejs Templates
import Login from './components/auth/Login';
import Dashboard from './components/dashboard/Dashboard';
import Account from './components/dashboard/Account';
import ResetPassword from './components/auth/ResetPassword';
import { Server, ServerConsole, ServerAllocations, ServerDatabases, ServerFiles, ServerSchedules, ServerSettings, ServerSubusers } from './components/server';
import {
Server, ServerAllocations, ServerConsole,
ServerDatabases,
ServerFiles,
ServerSchedules,
ServerSettings,
ServerSubusers
} from "./components/server";
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 },
{ path: '/server/:id', component: Server,
children: [
{ name: 'server', path: '', component: ServerConsole },
{ name: 'server-files', path: 'files', component: ServerFiles },
{ name: 'server-subusers', path: 'subusers', component: ServerSubusers },
{ name: 'server-schedules', path: 'schedules', component: ServerSchedules },
{ name: 'server-databases', path: 'databases', component: ServerDatabases },
{ name: 'server-allocations', path: 'allocations', component: ServerAllocations },
{ name: 'server-settings', path: 'settings', component: ServerSettings },
]
}
];

View File

@ -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;
}
}
};

View File

@ -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;
}
}
}

View File

@ -357,6 +357,13 @@ aws4@^1.2.1:
version "1.6.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
axios@^0.16:
version "0.16.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.16.2.tgz#ba4f92f17167dfbab40983785454b9ac149c3c6d"
dependencies:
follow-redirects "^1.2.3"
is-buffer "^1.1.5"
axios@^0.18.0:
version "0.18.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102"
@ -2152,6 +2159,12 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2:
inherits "^2.0.1"
readable-stream "^2.0.4"
follow-redirects@^1.2.3:
version "1.5.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.0.tgz#234f49cf770b7f35b40e790f636ceba0c3a0ab77"
dependencies:
debug "^3.1.0"
follow-redirects@^1.3.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa"
@ -3141,6 +3154,10 @@ just-debounce@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.0.0.tgz#87fccfaeffc0b68cd19d55f6722943f929ea35ea"
jwt-decode@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
kind-of@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-1.1.0.tgz#140a3d2d41a36d2efcfa9377b62c24f8495a5c44"
@ -3379,6 +3396,10 @@ lodash@^4.14.0, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0:
version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
lodash@^4.17:
version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
longest@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
@ -3610,6 +3631,10 @@ modify-filename@^1.0.0, modify-filename@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/modify-filename/-/modify-filename-1.1.0.tgz#9a2dec83806fbb2d975f22beec859ca26b393aa1"
moment@^2.18.1:
version "2.22.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.1.tgz#529a2e9bf973f259c9643d237fda84de3a26e8ad"
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@ -5855,10 +5880,18 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
validator@^8.1.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-8.2.0.tgz#3c1237290e37092355344fef78c231249dab77b9"
value-or-function@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813"
vee-validate@^2.0.9:
version "2.0.9"
resolved "https://registry.yarnpkg.com/vee-validate/-/vee-validate-2.0.9.tgz#948a96572d9e2369d5cb217a84269ecf6c1f9a11"
vendors@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22"
@ -5974,6 +6007,16 @@ vue-loader@^14.2.2:
vue-style-loader "^4.0.1"
vue-template-es2015-compiler "^1.6.0"
vue-mc@^0.2.4:
version "0.2.4"
resolved "https://registry.yarnpkg.com/vue-mc/-/vue-mc-0.2.4.tgz#93569cb6e08e2d1c52968a74cce8a6b2c9bda66a"
dependencies:
axios "^0.16"
lodash "^4.17"
moment "^2.18.1"
validator "^8.1.0"
vue "^2.2"
vue-router@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.1.tgz#d9b05ad9c7420ba0f626d6500d693e60092cc1e9"
@ -5996,7 +6039,7 @@ vue-template-es2015-compiler@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18"
vue@^2.5.7:
vue@^2.2, vue@^2.5.7:
version "2.5.16"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.16.tgz#07edb75e8412aaeed871ebafa99f4672584a0085"