Merge pull request #1 from pterodactyl/develop

Upstream Update
This commit is contained in:
Caleb 2020-08-16 12:00:54 -04:00 committed by GitHub
commit d3a544ac5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 498 additions and 239 deletions

View File

@ -12,6 +12,27 @@ What more are you waiting for? Make game servers a first class citizen on your p
![Image](https://cdn.pterodactyl.io/site-assets/mockup-macbook-grey.png)
## Sponsors
I would like to extend my sincere thanks to the following sponsors for funding Pterodactyl's developement. [Interested
in becoming a sponsor?](https://github.com/sponsors/DaneEveritt)
#### [BloomVPS](https://bloomvps.com)
> BloomVPS offers dedicated core VPS and Minecraft hosting with Ryzen 9 processors. With owned-hardware, we offer truly
> unbeatable prices on high-performance hosting.
#### [VersatileNode](https://versatilenode.com/)
> Looking to host a minecraft server, vps, or a website? VersatileNode is one of the most affordable hosting providers
> to provide quality yet cheap services with incredible support.
#### [MineStrator](https://minestrator.com/)
> Looking for a French highend hosting company for you minecraft server? More than 14,000 members on our discord
> trust us.
#### [DedicatedMC](https://dedicatedmc.io/)
> DedicatedMC provides Raw Power hosting at affordable pricing, making sure to never compromise on your performance
> and giving you the best performance money can buy.
## Support & Documentation
Support for using Pterodactyl can be found on our [Documentation Website](https://pterodactyl.io/project/introduction.html), [Guides Website](https://pterodactyl.io/community/about.html), or via our [Discord Chat](https://discord.gg/QRDZvVm).
@ -43,7 +64,7 @@ In addition to our standard nest of supported games, our community is constantly
## Credits
This software would not be possible without the work of other open-source authors who provide tools such as:
[Ace Editor](https://ace.c9.io), [AdminLTE](https://almsaeedstudio.com), [Animate.css](http://daneden.github.io/animate.css/), [AnsiUp](https://github.com/drudru/ansi_up), [Async.js](https://github.com/caolan/async),
[Ace Editor](https://ace.c9.io), [AdminLTE](https://adminlte.io), [Animate.css](http://daneden.github.io/animate.css/), [AnsiUp](https://github.com/drudru/ansi_up), [Async.js](https://github.com/caolan/async),
[Bootstrap](http://getbootstrap.com), [Bootstrap Notify](http://bootstrap-notify.remabledesigns.com), [Chart.js](http://www.chartjs.org), [FontAwesome](http://fontawesome.io),
[FontAwesome Animations](https://github.com/l-lin/font-awesome-animation), [jQuery](http://jquery.com), [Laravel](https://laravel.com), [Lodash](https://lodash.com),
[Select2](https://select2.github.io), [Socket.io](http://socket.io), [Socket.io File Upload](https://github.com/vote539/socketio-file-upload), [SweetAlert](http://t4t5.github.io/sweetalert),

View File

@ -2,6 +2,7 @@
namespace Pterodactyl\Exceptions\Http\Connection;
use Illuminate\Support\Arr;
use Illuminate\Http\Response;
use GuzzleHttp\Exception\GuzzleException;
use Pterodactyl\Exceptions\DisplayException;
@ -28,12 +29,28 @@ class DaemonConnectionException extends DisplayException
$response = method_exists($previous, 'getResponse') ? $previous->getResponse() : null;
if ($useStatusCode) {
$this->statusCode = is_null($response) ? 500 : $response->getStatusCode();
$this->statusCode = is_null($response) ? $this->statusCode : $response->getStatusCode();
}
parent::__construct(trans('admin/server.exceptions.daemon_exception', [
$message = trans('admin/server.exceptions.daemon_exception', [
'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(),
]), $previous, DisplayException::LEVEL_WARNING);
]);
// Attempt to pull the actual error message off the response and return that if it is not
// a 500 level error.
if ($this->statusCode < 500 && ! is_null($response)) {
$body = $response->getBody();
if (is_string($body) || (is_object($body) && method_exists($body, '__toString'))) {
$body = json_decode(is_string($body) ? $body : $body->__toString(), true);
$message = "[Wings Error]: " . Arr::get($body, 'error', $message);
}
}
$level = $this->statusCode >= 500 && $this->statusCode !== 504
? DisplayException::LEVEL_ERROR
: DisplayException::LEVEL_WARNING;
parent::__construct($message, $previous, $level);
}
/**

View File

@ -19,6 +19,7 @@ class BaseSettingsFormRequest extends AdminFormRequest
'app:name' => 'required|string|max:255',
'pterodactyl:auth:2fa_required' => 'required|integer|in:0,1,2',
'app:locale' => ['required', 'string', Rule::in(array_keys($this->getAvailableLanguages()))],
'app:analytics' => 'nullable|string',
];
}
@ -31,6 +32,7 @@ class BaseSettingsFormRequest extends AdminFormRequest
'app:name' => 'Company Name',
'pterodactyl:auth:2fa_required' => 'Require 2-Factor Authentication',
'app:locale' => 'Default Language',
'app:analytics' => 'Google Analytics',
];
}
}

View File

@ -37,6 +37,7 @@ class AssetComposer
'enabled' => config('recaptcha.enabled', false),
'siteKey' => config('recaptcha.website_key') ?? '',
],
'analytics' => config('app.analytics') ?? '',
]);
}
}

View File

@ -21,6 +21,7 @@ class SettingsServiceProvider extends ServiceProvider
protected $keys = [
'app:name',
'app:locale',
'app:analytics',
'recaptcha:enabled',
'recaptcha:secret_key',
'recaptcha:website_key',

View File

@ -230,6 +230,9 @@ class DaemonFileRepository extends DaemonRepository
'root' => $root ?? '/',
'files' => $files,
],
// Wait for up to 15 minutes for the archive to be completed when calling this endpoint
// since it will likely take quite awhile for large directories.
'timeout' => 60 * 15,
]
);
} catch (TransferException $exception) {

View File

@ -51,12 +51,29 @@ class EggConfigurationService
);
return [
'startup' => json_decode($server->egg->inherit_config_startup),
'startup' => $this->convertStartupToNewFormat(json_decode($server->egg->inherit_config_startup, true)),
'stop' => $this->convertStopToNewFormat($server->egg->inherit_config_stop),
'configs' => $configs,
];
}
/**
* Convert the "done" variable into an array if it is not currently one.
*
* @param array $startup
* @return array
*/
protected function convertStartupToNewFormat(array $startup)
{
$done = Arr::get($startup, 'done');
return [
'done' => is_string($done) ? [$done] : $done,
'user_interaction' => Arr::get($startup, 'userInteraction') ?? Arr::get($startup, 'user_interaction') ?? [],
'strip_ansi' => Arr::get($startup, 'strip_ansi') ?? false,
];
}
/**
* Converts a legacy stop string into a new generation stop option for a server.
*

View File

@ -27,11 +27,11 @@ class StatsTransformer extends BaseClientTransformer
'current_state' => Arr::get($data, 'state', 'stopped'),
'is_suspended' => Arr::get($data, 'suspended', false),
'resources' => [
'memory_bytes' => Arr::get($data, 'resources.memory_bytes', 0),
'cpu_absolute' => Arr::get($data, 'resources.cpu_absolute', 0),
'disk_bytes' => Arr::get($data, 'resources.disk_bytes', 0),
'network_rx_bytes' => Arr::get($data, 'resources.network.rx_bytes', 0),
'network_tx_bytes' => Arr::get($data, 'resources.network.tx_bytes', 0),
'memory_bytes' => Arr::get($data, 'memory_bytes', 0),
'cpu_absolute' => Arr::get($data, 'cpu_absolute', 0),
'disk_bytes' => Arr::get($data, 'disk_bytes', 0),
'network_rx_bytes' => Arr::get($data, 'network.rx_bytes', 0),
'network_tx_bytes' => Arr::get($data, 'network.tx_bytes', 0),
],
];
}

View File

@ -85,8 +85,8 @@ return [
| Configure the timeout to be used for Guzzle connections here.
*/
'guzzle' => [
'timeout' => env('GUZZLE_TIMEOUT', 5),
'connect_timeout' => env('GUZZLE_CONNECT_TIMEOUT', 3),
'timeout' => env('GUZZLE_TIMEOUT', 30),
'connect_timeout' => env('GUZZLE_CONNECT_TIMEOUT', 10),
],
/*

View File

@ -4,7 +4,6 @@
"@fortawesome/fontawesome-svg-core": "1.2.19",
"@fortawesome/free-solid-svg-icons": "^5.9.0",
"@fortawesome/react-fontawesome": "0.1.4",
"@types/react-google-recaptcha": "^1.1.1",
"axios": "^0.19.2",
"ayu-ace": "^2.0.4",
"brace": "^0.11.1",
@ -26,11 +25,14 @@
"react-dom": "npm:@hot-loader/react-dom",
"react-fast-compare": "^3.2.0",
"react-google-recaptcha": "^2.0.1",
"react-helmet": "^6.1.0",
"react-ga": "^3.1.2",
"react-hot-loader": "^4.12.21",
"react-i18next": "^11.2.1",
"react-redux": "^7.1.0",
"react-router-dom": "^5.1.2",
"react-transition-group": "^4.4.1",
"reaptcha": "^1.7.2",
"sockette": "^2.0.6",
"styled-components": "^5.1.1",
"styled-components-breakpoint": "^3.0.0-preview.20",
@ -61,6 +63,7 @@
"@types/query-string": "^6.3.0",
"@types/react": "^16.9.41",
"@types/react-dom": "^16.9.8",
"@types/react-helmet": "^6.0.0",
"@types/react-redux": "^7.1.1",
"@types/react-router": "^5.1.3",
"@types/react-router-dom": "^5.1.3",

View File

@ -1,8 +1,8 @@
import http from '@/api/http';
export default (email: string): Promise<string> => {
export default (email: string, recaptchaData?: string): Promise<string> => {
return new Promise((resolve, reject) => {
http.post('/auth/password', { email })
http.post('/auth/password', { email, 'g-recaptcha-response': recaptchaData })
.then(response => resolve(response.data.status || ''))
.catch(reject);
});

View File

@ -4,8 +4,8 @@ import { rawDataToFileObject } from '@/api/transformers';
export default async (uuid: string, directory: string, files: string[]): Promise<FileObject> => {
const { data } = await http.post(`/api/client/servers/${uuid}/files/compress`, { root: directory, files }, {
timeout: 300000,
timeoutErrorMessage: 'It looks like this archive is taking a long time to generate. It will appear when completed.',
timeout: 60000,
timeoutErrorMessage: 'It looks like this archive is taking a long time to generate. It will appear once completed.',
});
return rawDataToFileObject(data);

View File

@ -2,7 +2,7 @@ import http from '@/api/http';
import { rawDataToFileObject } from '@/api/transformers';
export interface FileObject {
uuid: string;
key: string;
name: string;
mode: string;
size: number;

View File

@ -1,7 +1,6 @@
import { Allocation } from '@/api/server/getServer';
import { FractalResponseData } from '@/api/http';
import { FileObject } from '@/api/server/files/loadDirectory';
import v4 from 'uuid/v4';
export const rawDataToServerAllocation = (data: FractalResponseData): Allocation => ({
id: data.attributes.id,
@ -13,7 +12,7 @@ export const rawDataToServerAllocation = (data: FractalResponseData): Allocation
});
export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({
uuid: v4(),
key: `${data.attributes.is_file ? 'file' : 'dir'}_${data.attributes.name}`,
name: data.attributes.name,
mode: data.attributes.mode,
size: Number(data.attributes.size),

View File

@ -32,4 +32,41 @@ export default createGlobalStyle`
input[type=number] {
-moz-appearance: textfield !important;
}
/* Scroll Bar Style */
::-webkit-scrollbar {
background: none;
width: 16px;
height: 16px;
}
::-webkit-scrollbar-thumb {
border: solid 0 rgb(0 0 0 / 0%);
border-right-width: 4px;
border-left-width: 4px;
-webkit-border-radius: 9px 4px;
-webkit-box-shadow: inset 0 0 0 1px hsl(211, 10%, 53%), inset 0 0 0 4px hsl(209deg 18% 30%);
}
::-webkit-scrollbar-track-piece {
margin: 4px 0;
}
::-webkit-scrollbar-thumb:horizontal {
border-right-width: 0;
border-left-width: 0;
border-top-width: 4px;
border-bottom-width: 4px;
-webkit-border-radius: 4px 9px;
}
::-webkit-scrollbar-thumb:hover {
-webkit-box-shadow:
inset 0 0 0 1px hsl(212, 92%, 43%),
inset 0 0 0 4px hsl(212, 92%, 43%);
}
::-webkit-scrollbar-corner {
background: transparent;
}
`;

View File

@ -1,4 +1,5 @@
import * as React from 'react';
import React, { useEffect } from 'react';
import ReactGA from 'react-ga';
import { hot } from 'react-hot-loader/root';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import { StoreProvider } from 'easy-peasy';
@ -48,6 +49,11 @@ const App = () => {
store.getActions().settings.setSettings(SiteConfiguration!);
}
useEffect(() => {
ReactGA.initialize(SiteConfiguration!.analytics);
ReactGA.pageview(location.pathname);
}, []);
return (
<>
<GlobalStylesheet/>

View File

@ -1,27 +1,40 @@
import * as React from 'react';
import { useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail';
import { httpErrorToHuman } from '@/api/http';
import LoginFormContainer from '@/components/auth/LoginFormContainer';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import { useStoreState } from 'easy-peasy';
import Field from '@/components/elements/Field';
import { Formik, FormikHelpers } from 'formik';
import { object, string } from 'yup';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
import Reaptcha from 'reaptcha';
import useFlash from '@/plugins/useFlash';
interface Values {
email: string;
}
export default () => {
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const ref = useRef<Reaptcha>(null);
const [ token, setToken ] = useState('');
const { clearFlashes, addFlash } = useFlash();
const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha);
const handleSubmission = ({ email }: Values, { setSubmitting, resetForm }: FormikHelpers<Values>) => {
setSubmitting(true);
clearFlashes();
requestPasswordResetEmail(email)
// If there is no token in the state yet, request the token and then abort this submit request
// since it will be re-submitted when the recaptcha data is returned by the component.
if (recaptchaEnabled && !token) {
ref.current!.execute().catch(error => console.error(error));
return;
}
requestPasswordResetEmail(email, token)
.then(response => {
resetForm();
addFlash({ type: 'success', title: 'Success', message: response });
@ -42,7 +55,7 @@ export default () => {
.required('A valid email address must be provided to continue.'),
})}
>
{({ isSubmitting }) => (
{({ isSubmitting, setSubmitting, submitForm }) => (
<LoginFormContainer
title={'Request Password Reset'}
css={tw`w-full flex`}
@ -64,6 +77,21 @@ export default () => {
Send Email
</Button>
</div>
{recaptchaEnabled &&
<Reaptcha
ref={ref}
size={'invisible'}
sitekey={siteKey || '_invalid_key'}
onVerify={response => {
setToken(response);
submitForm();
}}
onExpire={() => {
setSubmitting(false);
setToken('');
}}
/>
}
<div css={tw`mt-6 text-center`}>
<Link
type={'button'}

View File

@ -1,41 +1,67 @@
import React, { useRef } from 'react';
import React, { useRef, useState } from 'react';
import { Link, RouteComponentProps } from 'react-router-dom';
import login, { LoginData } from '@/api/auth/login';
import login from '@/api/auth/login';
import LoginFormContainer from '@/components/auth/LoginFormContainer';
import { ActionCreator, Actions, useStoreActions, useStoreState } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import { FormikProps, withFormik } from 'formik';
import { useStoreState } from 'easy-peasy';
import { Formik, FormikHelpers } from 'formik';
import { object, string } from 'yup';
import Field from '@/components/elements/Field';
import { httpErrorToHuman } from '@/api/http';
import { FlashMessage } from '@/state/flashes';
import ReCAPTCHA from 'react-google-recaptcha';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
import Reaptcha from 'reaptcha';
import useFlash from '@/plugins/useFlash';
type OwnProps = RouteComponentProps & {
clearFlashes: ActionCreator<void>;
addFlash: ActionCreator<FlashMessage>;
interface Values {
username: string;
password: string;
}
const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handleSubmit }: OwnProps & FormikProps<LoginData>) => {
const ref = useRef<ReCAPTCHA | null>(null);
const { enabled: recaptchaEnabled, siteKey } = useStoreState<ApplicationStore, any>(state => state.settings.data!.recaptcha);
const LoginContainer = ({ history }: RouteComponentProps) => {
const ref = useRef<Reaptcha>(null);
const [ token, setToken ] = useState('');
const submit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const { clearFlashes, clearAndAddHttpError } = useFlash();
const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha);
if (ref.current && !values.recaptchaData) {
return ref.current.execute();
const onSubmit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes();
// If there is no token in the state yet, request the token and then abort this submit request
// since it will be re-submitted when the recaptcha data is returned by the component.
if (recaptchaEnabled && !token) {
ref.current!.execute().catch(error => console.error(error));
return;
}
handleSubmit(e);
login({ ...values, recaptchaData: token })
.then(response => {
if (response.complete) {
// @ts-ignore
window.location = response.intended || '/';
return;
}
history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
})
.catch(error => {
console.error(error);
setSubmitting(false);
clearAndAddHttpError({ error });
});
};
return (
<React.Fragment>
{ref.current && ref.current.render()}
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`} onSubmit={submit}>
<Formik
onSubmit={onSubmit}
initialValues={{ username: '', password: '' }}
validationSchema={object().shape({
username: string().required('A username or email must be provided.'),
password: string().required('Please enter your account password.'),
})}
>
{({ isSubmitting, setSubmitting, submitForm }) => (
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`}>
<Field
type={'text'}
label={'Username or Email'}
@ -58,16 +84,18 @@ const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handl
</Button>
</div>
{recaptchaEnabled &&
<ReCAPTCHA
<Reaptcha
ref={ref}
size={'invisible'}
sitekey={siteKey || '_invalid_key'}
onChange={token => {
ref.current && ref.current.reset();
setFieldValue('recaptchaData', token);
onVerify={response => {
setToken(response);
submitForm();
}}
onExpired={() => setFieldValue('recaptchaData', null)}
onExpire={() => {
setSubmitting(false);
setToken('');
}}
/>
}
<div css={tw`mt-6 text-center`}>
@ -79,54 +107,9 @@ const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handl
</Link>
</div>
</LoginFormContainer>
</React.Fragment>
)}
</Formik>
);
};
const EnhancedForm = withFormik<OwnProps, LoginData>({
displayName: 'LoginContainerForm',
mapPropsToValues: () => ({
username: '',
password: '',
recaptchaData: null,
}),
validationSchema: () => object().shape({
username: string().required('A username or email must be provided.'),
password: string().required('Please enter your account password.'),
}),
handleSubmit: (values, { props, setFieldValue, setSubmitting }) => {
props.clearFlashes();
login(values)
.then(response => {
if (response.complete) {
// @ts-ignore
window.location = response.intended || '/';
return;
}
props.history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
})
.catch(error => {
console.error(error);
setSubmitting(false);
setFieldValue('recaptchaData', null);
props.addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
});
},
})(LoginContainer);
export default (props: RouteComponentProps) => {
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
return (
<EnhancedForm
{...props}
addFlash={addFlash}
clearFlashes={clearFlashes}
/>
);
};
export default LoginContainer;

View File

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import ContentBox from '@/components/elements/ContentBox';
import CreateApiKeyForm from '@/components/dashboard/forms/CreateApiKeyForm';
import getApiKeys, { ApiKey } from '@/api/account/getApiKeys';
@ -7,7 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faKey, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import ConfirmationModal from '@/components/elements/ConfirmationModal';
import deleteApiKey from '@/api/account/deleteApiKey';
import { Actions, useStoreActions } from 'easy-peasy';
import { Actions, useStoreActions, useStoreState } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import FlashMessageRender from '@/components/FlashMessageRender';
import { httpErrorToHuman } from '@/api/http';
@ -21,6 +22,7 @@ export default () => {
const [ keys, setKeys ] = useState<ApiKey[]>([]);
const [ loading, setLoading ] = useState(true);
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
useEffect(() => {
clearFlashes('account');
@ -49,6 +51,9 @@ export default () => {
return (
<PageContentBlock>
<Helmet>
<title> {name} | API</title>
</Helmet>
<FlashMessageRender byKey={'account'} css={tw`mb-4`}/>
<div css={tw`flex`}>
<ContentBox title={'Create API Key'} css={tw`flex-1`}>

View File

@ -1,4 +1,6 @@
import * as React from 'react';
import { Helmet } from 'react-helmet';
import { ApplicationStore } from '@/state';
import ContentBox from '@/components/elements/ContentBox';
import UpdatePasswordForm from '@/components/dashboard/forms/UpdatePasswordForm';
import UpdateEmailAddressForm from '@/components/dashboard/forms/UpdateEmailAddressForm';
@ -7,6 +9,7 @@ import PageContentBlock from '@/components/elements/PageContentBlock';
import tw from 'twin.macro';
import { breakpoint } from '@/theme';
import styled from 'styled-components/macro';
import { useStoreState } from 'easy-peasy';
const Container = styled.div`
${tw`flex flex-wrap my-10`};
@ -25,8 +28,12 @@ const Container = styled.div`
`;
export default () => {
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
return (
<PageContentBlock>
<Helmet>
<title> {name} | Account Overview</title>
</Helmet>
<Container>
<ContentBox title={'Update Password'} showFlashes={'account:password'}>
<UpdatePasswordForm/>

View File

@ -1,5 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import { Server } from '@/api/server/getServer';
import { ApplicationStore } from '@/state';
import getServers from '@/api/getServers';
import ServerRow from '@/components/dashboard/ServerRow';
import Spinner from '@/components/elements/Spinner';
@ -18,6 +20,7 @@ export default () => {
const [ page, setPage ] = useState(1);
const { rootAdmin } = useStoreState(state => state.user.data!);
const [ showOnlyAdmin, setShowOnlyAdmin ] = usePersistedState('show_all_servers', false);
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
const { data: servers, error } = useSWR<PaginatedResult<Server>>(
[ '/api/client/servers', showOnlyAdmin, page ],
@ -31,6 +34,9 @@ export default () => {
return (
<PageContentBlock showFlashKey={'dashboard'}>
<Helmet>
<title> {name} | Dashboard</title>
</Helmet>
{rootAdmin &&
<div css={tw`mb-2 flex justify-end items-center`}>
<p css={tw`uppercase text-xs text-neutral-400 mr-2`}>

View File

@ -0,0 +1,26 @@
import useWebsocketEvent from '@/plugins/useWebsocketEvent';
import { ServerContext } from '@/state/server';
import useServer from '@/plugins/useServer';
const InstallListener = () => {
const server = useServer();
const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
const setServer = ServerContext.useStoreActions(actions => actions.server.setServer);
// Listen for the installation completion event and then fire off a request to fetch the updated
// server information. This allows the server to automatically become available to the user if they
// just sit on the page.
useWebsocketEvent('install completed', () => {
getServer(server.uuid).catch(error => console.error(error));
});
// When we see the install started event immediately update the state to indicate such so that the
// screens automatically update.
useWebsocketEvent('install started', () => {
setServer({ ...server, isInstalling: true });
});
return null;
};
export default InstallListener;

View File

@ -1,4 +1,5 @@
import React, { lazy, useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import { ServerContext } from '@/state/server';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCircle, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons';
@ -61,6 +62,9 @@ export default () => {
return (
<PageContentBlock css={tw`flex`}>
<Helmet>
<title> {server.name} | Console </title>
</Helmet>
<div css={tw`w-1/4`}>
<TitledGreyBox title={server.name} icon={faServer}>
<p css={tw`text-xs uppercase`}>

View File

@ -8,7 +8,7 @@ const StopOrKillButton = ({ onPress }: { onPress: (action: PowerAction) => void
const status = ServerContext.useStoreState(state => state.status.value);
useEffect(() => {
setClicked(state => [ 'stopping' ].indexOf(status) < 0 ? false : state);
setClicked(status === 'stopping');
}, [ status ]);
return (

View File

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import Spinner from '@/components/elements/Spinner';
import getServerBackups from '@/api/server/backups/getServerBackups';
import useServer from '@/plugins/useServer';
@ -13,7 +14,7 @@ import PageContentBlock from '@/components/elements/PageContentBlock';
import tw from 'twin.macro';
export default () => {
const { uuid, featureLimits } = useServer();
const { uuid, featureLimits, name: serverName } = useServer();
const { addError, clearFlashes } = useFlash();
const [ loading, setLoading ] = useState(true);
@ -37,6 +38,9 @@ export default () => {
return (
<PageContentBlock>
<Helmet>
<title> {serverName} | Backups</title>
</Helmet>
<FlashMessageRender byKey={'backups'} css={tw`mb-4`}/>
{!backups.length ?
<p css={tw`text-center text-sm text-neutral-400`}>
@ -52,7 +56,7 @@ export default () => {
</div>
}
{featureLimits.backups === 0 &&
<p className="text-center text-sm text-neutral-400">
<p css={tw`text-center text-sm text-neutral-400`}>
Backups cannot be created for this server.
</p>
}

View File

@ -49,7 +49,7 @@ const ModalContent = ({ ...props }: RequiredModalProps) => {
</FormikFieldWrapper>
</div>
<div css={tw`flex justify-end`}>
<Button type={'submit'}>
<Button type={'submit'} disabled={isSubmitting}>
Start backup
</Button>
</div>
@ -94,11 +94,7 @@ export default () => {
ignored: string(),
})}
>
<ModalContent
appear
visible={visible}
onDismissed={() => setVisible(false)}
/>
<ModalContent appear visible={visible} onDismissed={() => setVisible(false)}/>
</Formik>
}
<Button onClick={() => setVisible(true)}>

View File

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import getServerDatabases from '@/api/server/getServerDatabases';
import { ServerContext } from '@/state/server';
import { httpErrorToHuman } from '@/api/http';
@ -14,7 +15,7 @@ import tw from 'twin.macro';
import Fade from '@/components/elements/Fade';
export default () => {
const { uuid, featureLimits } = useServer();
const { uuid, featureLimits, name: serverName } = useServer();
const { addError, clearFlashes } = useFlash();
const [ loading, setLoading ] = useState(true);
@ -36,6 +37,9 @@ export default () => {
return (
<PageContentBlock>
<Helmet>
<title> {serverName} | Databases </title>
</Helmet>
<FlashMessageRender byKey={'databases'} css={tw`mb-4`}/>
{(!databases.length && loading) ?
<Spinner size={'large'} centered/>

View File

@ -0,0 +1,10 @@
export enum SocketEvent {
DAEMON_MESSAGE = 'daemon message',
INSTALL_OUTPUT = 'install output',
INSTALL_STARTED = 'install started',
INSTALL_COMPLETED = 'install completed',
CONSOLE_OUTPUT = 'console output',
STATUS = 'status',
STATS = 'stats',
BACKUP_COMPLETED = 'backup completed',
}

View File

@ -1,4 +1,4 @@
import React, { useRef, useState } from 'react';
import React, { memo, useRef, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faBoxOpen,
@ -29,6 +29,7 @@ import styled from 'styled-components/macro';
import useEventListener from '@/plugins/useEventListener';
import compressFiles from '@/api/server/files/compressFiles';
import decompressFiles from '@/api/server/files/decompressFiles';
import isEqual from 'react-fast-compare';
type ModalType = 'rename' | 'move';
@ -50,7 +51,7 @@ const Row = ({ icon, title, ...props }: RowProps) => (
</StyledRow>
);
export default ({ file }: { file: FileObject }) => {
const FileDropdownMenu = ({ file }: { file: FileObject }) => {
const onClickRef = useRef<DropdownMenu>(null);
const [ showSpinner, setShowSpinner ] = useState(false);
const [ modal, setModal ] = useState<ModalType | null>(null);
@ -60,7 +61,7 @@ export default ({ file }: { file: FileObject }) => {
const { clearAndAddHttpError, clearFlashes } = useFlash();
const directory = ServerContext.useStoreState(state => state.files.directory);
useEventListener(`pterodactyl:files:ctx:${file.uuid}`, (e: CustomEvent) => {
useEventListener(`pterodactyl:files:ctx:${file.key}`, (e: CustomEvent) => {
if (onClickRef.current) {
onClickRef.current.triggerMenu(e.detail);
}
@ -71,7 +72,7 @@ export default ({ file }: { file: FileObject }) => {
// For UI speed, immediately remove the file from the listing before calling the deletion function.
// If the delete actually fails, we'll fetch the current directory contents again automatically.
mutate(files => files.filter(f => f.uuid !== file.uuid), false);
mutate(files => files.filter(f => f.key !== file.key), false);
deleteFiles(uuid, directory, [ file.name ]).catch(error => {
mutate();
@ -166,3 +167,5 @@ export default ({ file }: { file: FileObject }) => {
</DropdownMenu>
);
};
export default memo(FileDropdownMenu, isEqual);

View File

@ -1,4 +1,5 @@
import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet';
import { httpErrorToHuman } from '@/api/http';
import { CSSTransition } from 'react-transition-group';
import Spinner from '@/components/elements/Spinner';
@ -23,9 +24,10 @@ const sortFiles = (files: FileObject[]): FileObject[] => {
};
export default () => {
const { id } = useServer();
const { id, name: serverName } = useServer();
const { hash } = useLocation();
const { data: files, error, mutate } = useFileManagerSwr();
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles);
@ -42,6 +44,9 @@ export default () => {
return (
<PageContentBlock showFlashKey={'files'}>
<Helmet>
<title> {serverName} | File Manager </title>
</Helmet>
<FileManagerBreadcrumbs/>
{
!files ?
@ -65,7 +70,7 @@ export default () => {
}
{
sortFiles(files.slice(0, 250)).map(file => (
<FileObjectRow key={file.uuid} file={file}/>
<FileObjectRow key={file.key} file={file}/>
))
}
<MassActionsBar/>

View File

@ -39,7 +39,7 @@ const FileObjectRow = ({ file }: { file: FileObject }) => {
key={file.name}
onContextMenu={e => {
e.preventDefault();
window.dispatchEvent(new CustomEvent(`pterodactyl:files:ctx:${file.uuid}`, { detail: e.clientX }));
window.dispatchEvent(new CustomEvent(`pterodactyl:files:ctx:${file.key}`, { detail: e.clientX }));
}}
>
<SelectFileCheckbox name={file.name}/>

View File

@ -6,14 +6,12 @@ import Field from '@/components/elements/Field';
import { join } from 'path';
import { object, string } from 'yup';
import createDirectory from '@/api/server/files/createDirectory';
import v4 from 'uuid/v4';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
import { mutate } from 'swr';
import useServer from '@/plugins/useServer';
import { FileObject } from '@/api/server/files/loadDirectory';
import { useLocation } from 'react-router';
import useFlash from '@/plugins/useFlash';
import useFileManagerSwr from '@/plugins/useFileManagerSwr';
interface Values {
directoryName: string;
@ -24,7 +22,7 @@ const schema = object().shape({
});
const generateDirectoryData = (name: string): FileObject => ({
uuid: v4(),
key: `dir_${name}`,
name: name,
mode: '0644',
size: 0,
@ -39,20 +37,16 @@ const generateDirectoryData = (name: string): FileObject => ({
export default () => {
const { uuid } = useServer();
const { hash } = useLocation();
const { clearAndAddHttpError } = useFlash();
const [ visible, setVisible ] = useState(false);
const { mutate } = useFileManagerSwr();
const directory = ServerContext.useStoreState(state => state.files.directory);
const submit = ({ directoryName }: Values, { setSubmitting }: FormikHelpers<Values>) => {
createDirectory(uuid, directory, directoryName)
.then(() => {
mutate(
`${uuid}:files:${hash}`,
(data: FileObject[]) => [ ...data, generateDirectoryData(directoryName) ],
);
setVisible(false);
})
.then(() => mutate(data => [ ...data, generateDirectoryData(directoryName) ], false))
.then(() => setVisible(false))
.catch(error => {
console.error(error);
setSubmitting(false);
@ -79,6 +73,7 @@ export default () => {
>
<Form css={tw`m-0`}>
<Field
autoFocus
id={'directoryName'}
name={'directoryName'}
label={'Directory Name'}

View File

@ -15,9 +15,9 @@ interface FormikValues {
name: string;
}
type Props = RequiredModalProps & { files: string[]; useMoveTerminology?: boolean };
type OwnProps = RequiredModalProps & { files: string[]; useMoveTerminology?: boolean };
export default ({ files, useMoveTerminology, ...props }: Props) => {
const RenameFileModal = ({ files, useMoveTerminology, ...props }: OwnProps) => {
const { uuid } = useServer();
const { mutate } = useFileManagerSwr();
const { clearFlashes, clearAndAddHttpError } = useFlash();
@ -96,3 +96,5 @@ export default ({ files, useMoveTerminology, ...props }: Props) => {
</Formik>
);
};
export default RenameFileModal;

View File

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import tw from 'twin.macro';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faNetworkWired } from '@fortawesome/free-solid-svg-icons';
@ -23,7 +24,7 @@ const Code = styled.code`${tw`font-mono py-1 px-2 bg-neutral-900 rounded text-sm
const Label = styled.label`${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}`;
const NetworkContainer = () => {
const { uuid, allocations } = useServer();
const { uuid, allocations, name: serverName } = useServer();
const { clearFlashes, clearAndAddHttpError } = useFlash();
const [ loading, setLoading ] = useState<false | number>(false);
const { data, error, mutate } = useSWR<Allocation[]>(uuid, key => getServerAllocations(key), { initialData: allocations });
@ -61,6 +62,9 @@ const NetworkContainer = () => {
return (
<PageContentBlock showFlashKey={'server:network'}>
<Helmet>
<title> {serverName} | Network </title>
</Helmet>
{!data ?
<Spinner size={'large'} centered/>
:

View File

@ -65,7 +65,7 @@ const EditScheduleModal = ({ schedule, ...props }: Omit<Props, 'onScheduleUpdate
/>
</div>
<div css={tw`mt-6 text-right`}>
<Button type={'submit'}>
<Button type={'submit'} disabled={isSubmitting}>
{schedule ? 'Save changes' : 'Create schedule'}
</Button>
</div>

View File

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import getServerSchedules from '@/api/server/schedules/getServerSchedules';
import { ServerContext } from '@/state/server';
import Spinner from '@/components/elements/Spinner';
@ -16,7 +17,7 @@ import GreyRowBox from '@/components/elements/GreyRowBox';
import Button from '@/components/elements/Button';
export default ({ match, history }: RouteComponentProps) => {
const { uuid } = useServer();
const { uuid, name: serverName } = useServer();
const { clearFlashes, addError } = useFlash();
const [ loading, setLoading ] = useState(true);
const [ visible, setVisible ] = useState(false);
@ -37,6 +38,9 @@ export default ({ match, history }: RouteComponentProps) => {
return (
<PageContentBlock>
<Helmet>
<title> {serverName} | Schedules </title>
</Helmet>
<FlashMessageRender byKey={'schedules'} css={tw`mb-4`}/>
{(!schedules.length && loading) ?
<Spinner size={'large'} centered/>

View File

@ -14,7 +14,7 @@ export default ({ schedule }: { schedule: Schedule }) => (
<p>{schedule.name}</p>
<p css={tw`text-xs text-neutral-400`}>
Last run
at: {schedule.lastRunAt ? format(schedule.lastRunAt, 'MMM Do [at] h:mma') : 'never'}
at: {schedule.lastRunAt ? format(schedule.lastRunAt, 'MMM do \'at\' h:mma') : 'never'}
</p>
</div>
<div css={tw`flex items-center mx-8`}>

View File

@ -32,11 +32,16 @@ interface Values {
}
const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
const { values: { action }, setFieldValue, setFieldTouched } = useFormikContext<Values>();
const { values: { action }, initialValues, setFieldValue, setFieldTouched, isSubmitting } = useFormikContext<Values>();
useEffect(() => {
if (action !== initialValues.action) {
setFieldValue('payload', action === 'power' ? 'start' : '');
setFieldTouched('payload', false);
} else {
setFieldValue('payload', initialValues.payload);
setFieldTouched('payload', false);
}
}, [ action ]);
return (
@ -94,7 +99,7 @@ const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
/>
</div>
<div css={tw`flex justify-end mt-6`}>
<Button type={'submit'}>
<Button type={'submit'} disabled={isSubmitting}>
{isEditingTask ? 'Save Changes' : 'Create Task'}
</Button>
</div>

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { ServerContext } from '@/state/server';
import TitledGreyBox from '@/components/elements/TitledGreyBox';
import ConfirmationModal from '@/components/elements/ConfirmationModal';
@ -37,6 +37,10 @@ export default () => {
});
};
useEffect(() => {
clearFlashes();
}, []);
return (
<TitledGreyBox title={'Reinstall Server'} css={tw`relative`}>
<ConfirmationModal

View File

@ -1,4 +1,5 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import TitledGreyBox from '@/components/elements/TitledGreyBox';
import { ServerContext } from '@/state/server';
import { useStoreState } from 'easy-peasy';
@ -20,6 +21,9 @@ export default () => {
return (
<PageContentBlock>
<Helmet>
<title> {server.name} | Settings </title>
</Helmet>
<FlashMessageRender byKey={'settings'} css={tw`mb-4`}/>
<div css={tw`md:flex`}>
<div css={tw`w-full md:flex-1 md:mr-10`}>

View File

@ -51,17 +51,18 @@ export default ({ subuser }: Props) => {
</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Permissions</p>
</div>
<Can action={'user.update'}>
{subuser.uuid !== uuid &&
<button
type={'button'}
aria-label={'Edit subuser'}
css={[
tw`block text-sm p-2 text-neutral-500 hover:text-neutral-100 transition-colors duration-150 mx-4`,
subuser.uuid === uuid ? tw`hidden` : undefined,
]}
css={tw`block text-sm p-2 text-neutral-500 hover:text-neutral-100 transition-colors duration-150 mx-4`}
onClick={() => setVisible(true)}
>
<FontAwesomeIcon icon={faPencilAlt}/>
</button>
}
</Can>
<Can action={'user.delete'}>
<RemoveSubuserButton subuser={subuser}/>
</Can>

View File

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import { ServerContext } from '@/state/server';
import { Actions, useStoreActions, useStoreState } from 'easy-peasy';
import { ApplicationStore } from '@/state';
@ -17,6 +18,7 @@ export default () => {
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
const subusers = ServerContext.useStoreState(state => state.subusers.data);
const servername = ServerContext.useStoreState(state => state.server.data!.name);
const setSubusers = ServerContext.useStoreActions(actions => actions.subusers.setSubusers);
const permissions = useStoreState((state: ApplicationStore) => state.permissions.data);
@ -49,6 +51,9 @@ export default () => {
return (
<PageContentBlock>
<Helmet>
<title> {servername} | Subusers </title>
</Helmet>
<FlashMessageRender byKey={'users'} css={tw`mb-4`}/>
{!subusers.length ?
<p css={tw`text-center text-sm text-neutral-400`}>

View File

@ -1,13 +1,6 @@
import Sockette from 'sockette';
import { EventEmitter } from 'events';
export const SOCKET_EVENTS = [
'SOCKET_OPEN',
'SOCKET_RECONNECT',
'SOCKET_CLOSE',
'SOCKET_ERROR',
];
export class Websocket extends EventEmitter {
// Timer instance for this socket.
private timer: any = null;

View File

@ -1,9 +1,8 @@
import { DependencyList } from 'react';
import { ServerContext } from '@/state/server';
import { Server } from '@/api/server/getServer';
const useServer = (dependencies?: DependencyList): Server => {
return ServerContext.useStoreState(state => state.server.data!, [ dependencies ]);
const useServer = (dependencies?: any[] | undefined): Server => {
return ServerContext.useStoreState(state => state.server.data!, dependencies);
};
export default useServer;

View File

@ -1,4 +1,5 @@
import React from 'react';
import React, { useEffect } from 'react';
import ReactGA from 'react-ga';
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
import LoginContainer from '@/components/auth/LoginContainer';
import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer';
@ -6,7 +7,12 @@ import ResetPasswordContainer from '@/components/auth/ResetPasswordContainer';
import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer';
import NotFound from '@/components/screens/NotFound';
export default ({ location, history, match }: RouteComponentProps) => (
export default ({ location, history, match }: RouteComponentProps) => {
useEffect(() => {
ReactGA.pageview(location.pathname);
}, [ location.pathname ]);
return (
<div className={'pt-8 xl:pt-32'}>
<Switch location={location}>
<Route path={`${match.path}/login`} component={LoginContainer} exact/>
@ -20,3 +26,4 @@ export default ({ location, history, match }: RouteComponentProps) => (
</Switch>
</div>
);
};

View File

@ -1,4 +1,5 @@
import * as React from 'react';
import React, { useEffect } from 'react';
import ReactGA from 'react-ga';
import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom';
import AccountOverviewContainer from '@/components/dashboard/AccountOverviewContainer';
import NavigationBar from '@/components/NavigationBar';
@ -8,7 +9,12 @@ import NotFound from '@/components/screens/NotFound';
import TransitionRouter from '@/TransitionRouter';
import SubNavigation from '@/components/elements/SubNavigation';
export default ({ location }: RouteComponentProps) => (
export default ({ location }: RouteComponentProps) => {
useEffect(() => {
ReactGA.pageview(location.pathname);
}, [ location.pathname ]);
return (
<>
<NavigationBar />
{location.pathname.startsWith('/account') &&
@ -29,3 +35,4 @@ export default ({ location }: RouteComponentProps) => (
</TransitionRouter>
</>
);
};

View File

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import ReactGA from 'react-ga';
import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom';
import NavigationBar from '@/components/NavigationBar';
import ServerConsole from '@/components/server/ServerConsole';
@ -25,6 +26,7 @@ import useServer from '@/plugins/useServer';
import ScreenBlock from '@/components/screens/ScreenBlock';
import SubNavigation from '@/components/elements/SubNavigation';
import NetworkContainer from '@/components/server/network/NetworkContainer';
import InstallListener from '@/components/server/InstallListener';
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
const { rootAdmin } = useStoreState(state => state.user.data!);
@ -60,6 +62,10 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
};
}, [ match.params.id ]);
useEffect(() => {
ReactGA.pageview(location.pathname);
}, [ location.pathname ]);
return (
<React.Fragment key={'server-router'}>
<NavigationBar/>
@ -98,6 +104,8 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
</div>
</SubNavigation>
</CSSTransition>
<InstallListener/>
<WebsocketHandler/>
{(installing && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${server.id}`)))) ?
<ScreenBlock
title={'Your server is installing.'}
@ -106,7 +114,6 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
/>
:
<>
<WebsocketHandler/>
<TransitionRouter>
<Switch location={location}>
<Route path={`${match.path}`} component={ServerConsole} exact/>

View File

@ -6,7 +6,7 @@ export interface FlashStore {
items: FlashMessage[];
addFlash: Action<FlashStore, FlashMessage>;
addError: Action<FlashStore, { message: string; key?: string }>;
clearAndAddHttpError: Action<FlashStore, { error: any, key: string }>;
clearAndAddHttpError: Action<FlashStore, { error: any, key?: string }>;
clearFlashes: Action<FlashStore, string | void>;
}

View File

@ -7,6 +7,7 @@ export interface SiteSettings {
enabled: boolean;
siteKey: string;
};
analytics: string;
}
export interface SettingsStore {

View File

@ -31,6 +31,13 @@
<p class="text-muted"><small>This is the name that is used throughout the panel and in emails sent to clients.</small></p>
</div>
</div>
<div class="form-group col-md-4">
<label class="control-label">Google Analytics</label>
<div>
<input type="text" class="form-control" name="app:analytics" value="{{ old('app:analytics', config('app.analytics')) }}" />
<p class="text-muted"><small>This is your Google Analytics Tracking ID, Ex. UA-123723645-2</small></p>
</div>
</div>
<div class="form-group col-md-4">
<label class="control-label">Require 2-Factor Authentication</label>
<div>

View File

@ -26,7 +26,7 @@ Route::group(['middleware' => 'guest'], function () {
// Password reset routes. This endpoint is hit after going through
// the forgot password routes to acquire a token (or after an account
// is created).
Route::post('/password/reset', 'ResetPasswordController')->name('auth.reset-password')->middleware('recaptcha');
Route::post('/password/reset', 'ResetPasswordController')->name('auth.reset-password');
// Catch any other combinations of routes and pass them off to the Vuejs component.
Route::fallback('LoginController@index');

View File

@ -1013,9 +1013,10 @@
dependencies:
"@types/react" "*"
"@types/react-google-recaptcha@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/react-google-recaptcha/-/react-google-recaptcha-1.1.1.tgz#7dd2a4dd15d38d8059a2753cd4a7e3485c9bb3ea"
"@types/react-helmet@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.0.0.tgz#5b74e44a12662ffb12d1c97ee702cf4e220958cf"
integrity sha512-NBMPAxgjpaMooXa51cU1BTgrX6T+hQbMiLm77JhBbfOzPQea3RB5rNpPOD5xGWHIVpGXHd59cltEzIq0qglGcQ==
dependencies:
"@types/react" "*"
@ -5564,11 +5565,16 @@ react-fast-compare@^2.0.1:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
react-fast-compare@^3.2.0:
react-fast-compare@^3.1.1, react-fast-compare@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
react-ga@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-3.1.2.tgz#e13f211c51a2e5c401ea69cf094b9501fe3c51ce"
integrity sha512-OJrMqaHEHbodm+XsnjA6ISBEHTwvpFrxco65mctzl/v3CASMSLSyUkFqz9yYrPDKGBUfNQzKCjuMJwctjlWBbw==
react-google-recaptcha@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-2.0.1.tgz#3276b29659493f7ca2a5b7739f6c239293cdf1d8"
@ -5576,6 +5582,16 @@ react-google-recaptcha@^2.0.1:
prop-types "^15.5.0"
react-async-script "^1.1.1"
react-helmet@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726"
integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==
dependencies:
object-assign "^4.1.1"
prop-types "^15.7.2"
react-fast-compare "^3.1.1"
react-side-effect "^2.1.0"
react-hot-loader@^4.12.21:
version "4.12.21"
resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.21.tgz#332e830801fb33024b5a147d6b13417f491eb975"
@ -5643,6 +5659,11 @@ react-router@5.1.2:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-side-effect@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.0.tgz#1ce4a8b4445168c487ed24dab886421f74d380d3"
integrity sha512-IgmcegOSi5SNX+2Snh1vqmF0Vg/CbkycU9XZbOHJlZ6kMzTmi3yc254oB1WCkgA7OQtIAoLmcSFuHTc/tlcqXg==
react-transition-group@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
@ -5714,6 +5735,11 @@ readdirp@~3.4.0:
dependencies:
picomatch "^2.2.1"
reaptcha@^1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/reaptcha/-/reaptcha-1.7.2.tgz#d829f54270c241f46501e92a5a7badeb1fcf372d"
integrity sha512-/RXiPeMd+fPUGByv+kAaQlCXCsSflZ9bKX5Fcwv9IYGS1oyT2nntL/8zn9IaiUFHL66T1jBtOABcb92g2+3w8w==
reduce-css-calc@^2.1.6:
version "2.1.7"
resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.7.tgz#1ace2e02c286d78abcd01fd92bfe8097ab0602c2"