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) ![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 & 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). 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 ## Credits
This software would not be possible without the work of other open-source authors who provide tools such as: 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), [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), [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), [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; namespace Pterodactyl\Exceptions\Http\Connection;
use Illuminate\Support\Arr;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\GuzzleException;
use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayException;
@ -28,12 +29,28 @@ class DaemonConnectionException extends DisplayException
$response = method_exists($previous, 'getResponse') ? $previous->getResponse() : null; $response = method_exists($previous, 'getResponse') ? $previous->getResponse() : null;
if ($useStatusCode) { 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(), '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', 'app:name' => 'required|string|max:255',
'pterodactyl:auth:2fa_required' => 'required|integer|in:0,1,2', 'pterodactyl:auth:2fa_required' => 'required|integer|in:0,1,2',
'app:locale' => ['required', 'string', Rule::in(array_keys($this->getAvailableLanguages()))], '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', 'app:name' => 'Company Name',
'pterodactyl:auth:2fa_required' => 'Require 2-Factor Authentication', 'pterodactyl:auth:2fa_required' => 'Require 2-Factor Authentication',
'app:locale' => 'Default Language', 'app:locale' => 'Default Language',
'app:analytics' => 'Google Analytics',
]; ];
} }
} }

View file

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

View file

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

View file

@ -230,6 +230,9 @@ class DaemonFileRepository extends DaemonRepository
'root' => $root ?? '/', 'root' => $root ?? '/',
'files' => $files, '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) { } catch (TransferException $exception) {

View file

@ -51,12 +51,29 @@ class EggConfigurationService
); );
return [ 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), 'stop' => $this->convertStopToNewFormat($server->egg->inherit_config_stop),
'configs' => $configs, '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. * 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'), 'current_state' => Arr::get($data, 'state', 'stopped'),
'is_suspended' => Arr::get($data, 'suspended', false), 'is_suspended' => Arr::get($data, 'suspended', false),
'resources' => [ 'resources' => [
'memory_bytes' => Arr::get($data, 'resources.memory_bytes', 0), 'memory_bytes' => Arr::get($data, 'memory_bytes', 0),
'cpu_absolute' => Arr::get($data, 'resources.cpu_absolute', 0), 'cpu_absolute' => Arr::get($data, 'cpu_absolute', 0),
'disk_bytes' => Arr::get($data, 'resources.disk_bytes', 0), 'disk_bytes' => Arr::get($data, 'disk_bytes', 0),
'network_rx_bytes' => Arr::get($data, 'resources.network.rx_bytes', 0), 'network_rx_bytes' => Arr::get($data, 'network.rx_bytes', 0),
'network_tx_bytes' => Arr::get($data, 'resources.network.tx_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. | Configure the timeout to be used for Guzzle connections here.
*/ */
'guzzle' => [ 'guzzle' => [
'timeout' => env('GUZZLE_TIMEOUT', 5), 'timeout' => env('GUZZLE_TIMEOUT', 30),
'connect_timeout' => env('GUZZLE_CONNECT_TIMEOUT', 3), 'connect_timeout' => env('GUZZLE_CONNECT_TIMEOUT', 10),
], ],
/* /*

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,4 +32,41 @@ export default createGlobalStyle`
input[type=number] { input[type=number] {
-moz-appearance: textfield !important; -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 { hot } from 'react-hot-loader/root';
import { BrowserRouter, Route, Switch } from 'react-router-dom'; import { BrowserRouter, Route, Switch } from 'react-router-dom';
import { StoreProvider } from 'easy-peasy'; import { StoreProvider } from 'easy-peasy';
@ -48,6 +49,11 @@ const App = () => {
store.getActions().settings.setSettings(SiteConfiguration!); store.getActions().settings.setSettings(SiteConfiguration!);
} }
useEffect(() => {
ReactGA.initialize(SiteConfiguration!.analytics);
ReactGA.pageview(location.pathname);
}, []);
return ( return (
<> <>
<GlobalStylesheet/> <GlobalStylesheet/>

View file

@ -1,27 +1,40 @@
import * as React from 'react'; import * as React from 'react';
import { useRef, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail'; import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
import LoginFormContainer from '@/components/auth/LoginFormContainer'; import LoginFormContainer from '@/components/auth/LoginFormContainer';
import { Actions, useStoreActions } from 'easy-peasy'; import { useStoreState } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import Field from '@/components/elements/Field'; import Field from '@/components/elements/Field';
import { Formik, FormikHelpers } from 'formik'; import { Formik, FormikHelpers } from 'formik';
import { object, string } from 'yup'; import { object, string } from 'yup';
import tw from 'twin.macro'; import tw from 'twin.macro';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import Reaptcha from 'reaptcha';
import useFlash from '@/plugins/useFlash';
interface Values { interface Values {
email: string; email: string;
} }
export default () => { 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>) => { const handleSubmission = ({ email }: Values, { setSubmitting, resetForm }: FormikHelpers<Values>) => {
setSubmitting(true);
clearFlashes(); 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 => { .then(response => {
resetForm(); resetForm();
addFlash({ type: 'success', title: 'Success', message: response }); addFlash({ type: 'success', title: 'Success', message: response });
@ -42,7 +55,7 @@ export default () => {
.required('A valid email address must be provided to continue.'), .required('A valid email address must be provided to continue.'),
})} })}
> >
{({ isSubmitting }) => ( {({ isSubmitting, setSubmitting, submitForm }) => (
<LoginFormContainer <LoginFormContainer
title={'Request Password Reset'} title={'Request Password Reset'}
css={tw`w-full flex`} css={tw`w-full flex`}
@ -64,6 +77,21 @@ export default () => {
Send Email Send Email
</Button> </Button>
</div> </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`}> <div css={tw`mt-6 text-center`}>
<Link <Link
type={'button'} 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 { 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 LoginFormContainer from '@/components/auth/LoginFormContainer';
import { ActionCreator, Actions, useStoreActions, useStoreState } from 'easy-peasy'; import { useStoreState } from 'easy-peasy';
import { ApplicationStore } from '@/state'; import { Formik, FormikHelpers } from 'formik';
import { FormikProps, withFormik } from 'formik';
import { object, string } from 'yup'; import { object, string } from 'yup';
import Field from '@/components/elements/Field'; 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 tw from 'twin.macro';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import Reaptcha from 'reaptcha';
import useFlash from '@/plugins/useFlash';
type OwnProps = RouteComponentProps & { interface Values {
clearFlashes: ActionCreator<void>; username: string;
addFlash: ActionCreator<FlashMessage>; password: string;
} }
const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handleSubmit }: OwnProps & FormikProps<LoginData>) => { const LoginContainer = ({ history }: RouteComponentProps) => {
const ref = useRef<ReCAPTCHA | null>(null); const ref = useRef<Reaptcha>(null);
const { enabled: recaptchaEnabled, siteKey } = useStoreState<ApplicationStore, any>(state => state.settings.data!.recaptcha); const [ token, setToken ] = useState('');
const submit = (e: React.FormEvent<HTMLFormElement>) => { const { clearFlashes, clearAndAddHttpError } = useFlash();
e.preventDefault(); const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha);
if (ref.current && !values.recaptchaData) { const onSubmit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
return ref.current.execute(); 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 ( return (
<React.Fragment> <Formik
{ref.current && ref.current.render()} onSubmit={onSubmit}
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`} onSubmit={submit}> 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 <Field
type={'text'} type={'text'}
label={'Username or Email'} label={'Username or Email'}
@ -58,16 +84,18 @@ const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handl
</Button> </Button>
</div> </div>
{recaptchaEnabled && {recaptchaEnabled &&
<ReCAPTCHA <Reaptcha
ref={ref} ref={ref}
size={'invisible'} size={'invisible'}
sitekey={siteKey || '_invalid_key'} sitekey={siteKey || '_invalid_key'}
onChange={token => { onVerify={response => {
ref.current && ref.current.reset(); setToken(response);
setFieldValue('recaptchaData', token);
submitForm(); submitForm();
}} }}
onExpired={() => setFieldValue('recaptchaData', null)} onExpire={() => {
setSubmitting(false);
setToken('');
}}
/> />
} }
<div css={tw`mt-6 text-center`}> <div css={tw`mt-6 text-center`}>
@ -79,54 +107,9 @@ const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handl
</Link> </Link>
</div> </div>
</LoginFormContainer> </LoginFormContainer>
</React.Fragment> )}
</Formik>
); );
}; };
const EnhancedForm = withFormik<OwnProps, LoginData>({ export default LoginContainer;
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}
/>
);
};

View file

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

View file

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

View file

@ -1,5 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import { Server } from '@/api/server/getServer'; import { Server } from '@/api/server/getServer';
import { ApplicationStore } from '@/state';
import getServers from '@/api/getServers'; import getServers from '@/api/getServers';
import ServerRow from '@/components/dashboard/ServerRow'; import ServerRow from '@/components/dashboard/ServerRow';
import Spinner from '@/components/elements/Spinner'; import Spinner from '@/components/elements/Spinner';
@ -18,6 +20,7 @@ export default () => {
const [ page, setPage ] = useState(1); const [ page, setPage ] = useState(1);
const { rootAdmin } = useStoreState(state => state.user.data!); const { rootAdmin } = useStoreState(state => state.user.data!);
const [ showOnlyAdmin, setShowOnlyAdmin ] = usePersistedState('show_all_servers', false); const [ showOnlyAdmin, setShowOnlyAdmin ] = usePersistedState('show_all_servers', false);
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
const { data: servers, error } = useSWR<PaginatedResult<Server>>( const { data: servers, error } = useSWR<PaginatedResult<Server>>(
[ '/api/client/servers', showOnlyAdmin, page ], [ '/api/client/servers', showOnlyAdmin, page ],
@ -31,6 +34,9 @@ export default () => {
return ( return (
<PageContentBlock showFlashKey={'dashboard'}> <PageContentBlock showFlashKey={'dashboard'}>
<Helmet>
<title> {name} | Dashboard</title>
</Helmet>
{rootAdmin && {rootAdmin &&
<div css={tw`mb-2 flex justify-end items-center`}> <div css={tw`mb-2 flex justify-end items-center`}>
<p css={tw`uppercase text-xs text-neutral-400 mr-2`}> <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 React, { lazy, useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCircle, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons'; import { faCircle, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons';
@ -61,6 +62,9 @@ export default () => {
return ( return (
<PageContentBlock css={tw`flex`}> <PageContentBlock css={tw`flex`}>
<Helmet>
<title> {server.name} | Console </title>
</Helmet>
<div css={tw`w-1/4`}> <div css={tw`w-1/4`}>
<TitledGreyBox title={server.name} icon={faServer}> <TitledGreyBox title={server.name} icon={faServer}>
<p css={tw`text-xs uppercase`}> <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); const status = ServerContext.useStoreState(state => state.status.value);
useEffect(() => { useEffect(() => {
setClicked(state => [ 'stopping' ].indexOf(status) < 0 ? false : state); setClicked(status === 'stopping');
}, [ status ]); }, [ status ]);
return ( return (

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import getServerDatabases from '@/api/server/getServerDatabases'; import getServerDatabases from '@/api/server/getServerDatabases';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
@ -14,7 +15,7 @@ import tw from 'twin.macro';
import Fade from '@/components/elements/Fade'; import Fade from '@/components/elements/Fade';
export default () => { export default () => {
const { uuid, featureLimits } = useServer(); const { uuid, featureLimits, name: serverName } = useServer();
const { addError, clearFlashes } = useFlash(); const { addError, clearFlashes } = useFlash();
const [ loading, setLoading ] = useState(true); const [ loading, setLoading ] = useState(true);
@ -36,6 +37,9 @@ export default () => {
return ( return (
<PageContentBlock> <PageContentBlock>
<Helmet>
<title> {serverName} | Databases </title>
</Helmet>
<FlashMessageRender byKey={'databases'} css={tw`mb-4`}/> <FlashMessageRender byKey={'databases'} css={tw`mb-4`}/>
{(!databases.length && loading) ? {(!databases.length && loading) ?
<Spinner size={'large'} centered/> <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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { import {
faBoxOpen, faBoxOpen,
@ -29,6 +29,7 @@ import styled from 'styled-components/macro';
import useEventListener from '@/plugins/useEventListener'; import useEventListener from '@/plugins/useEventListener';
import compressFiles from '@/api/server/files/compressFiles'; import compressFiles from '@/api/server/files/compressFiles';
import decompressFiles from '@/api/server/files/decompressFiles'; import decompressFiles from '@/api/server/files/decompressFiles';
import isEqual from 'react-fast-compare';
type ModalType = 'rename' | 'move'; type ModalType = 'rename' | 'move';
@ -50,7 +51,7 @@ const Row = ({ icon, title, ...props }: RowProps) => (
</StyledRow> </StyledRow>
); );
export default ({ file }: { file: FileObject }) => { const FileDropdownMenu = ({ file }: { file: FileObject }) => {
const onClickRef = useRef<DropdownMenu>(null); const onClickRef = useRef<DropdownMenu>(null);
const [ showSpinner, setShowSpinner ] = useState(false); const [ showSpinner, setShowSpinner ] = useState(false);
const [ modal, setModal ] = useState<ModalType | null>(null); const [ modal, setModal ] = useState<ModalType | null>(null);
@ -60,7 +61,7 @@ export default ({ file }: { file: FileObject }) => {
const { clearAndAddHttpError, clearFlashes } = useFlash(); const { clearAndAddHttpError, clearFlashes } = useFlash();
const directory = ServerContext.useStoreState(state => state.files.directory); 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) { if (onClickRef.current) {
onClickRef.current.triggerMenu(e.detail); 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. // 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. // 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 => { deleteFiles(uuid, directory, [ file.name ]).catch(error => {
mutate(); mutate();
@ -166,3 +167,5 @@ export default ({ file }: { file: FileObject }) => {
</DropdownMenu> </DropdownMenu>
); );
}; };
export default memo(FileDropdownMenu, isEqual);

View file

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

View file

@ -39,7 +39,7 @@ const FileObjectRow = ({ file }: { file: FileObject }) => {
key={file.name} key={file.name}
onContextMenu={e => { onContextMenu={e => {
e.preventDefault(); 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}/> <SelectFileCheckbox name={file.name}/>

View file

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

View file

@ -15,9 +15,9 @@ interface FormikValues {
name: string; 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 { uuid } = useServer();
const { mutate } = useFileManagerSwr(); const { mutate } = useFileManagerSwr();
const { clearFlashes, clearAndAddHttpError } = useFlash(); const { clearFlashes, clearAndAddHttpError } = useFlash();
@ -96,3 +96,5 @@ export default ({ files, useMoveTerminology, ...props }: Props) => {
</Formik> </Formik>
); );
}; };
export default RenameFileModal;

View file

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

View file

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

View file

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

View file

@ -14,7 +14,7 @@ export default ({ schedule }: { schedule: Schedule }) => (
<p>{schedule.name}</p> <p>{schedule.name}</p>
<p css={tw`text-xs text-neutral-400`}> <p css={tw`text-xs text-neutral-400`}>
Last run 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> </p>
</div> </div>
<div css={tw`flex items-center mx-8`}> <div css={tw`flex items-center mx-8`}>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,8 @@
import { DependencyList } from 'react';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import { Server } from '@/api/server/getServer'; import { Server } from '@/api/server/getServer';
const useServer = (dependencies?: DependencyList): Server => { const useServer = (dependencies?: any[] | undefined): Server => {
return ServerContext.useStoreState(state => state.server.data!, [ dependencies ]); return ServerContext.useStoreState(state => state.server.data!, dependencies);
}; };
export default useServer; 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 { Route, RouteComponentProps, Switch } from 'react-router-dom';
import LoginContainer from '@/components/auth/LoginContainer'; import LoginContainer from '@/components/auth/LoginContainer';
import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer'; import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer';
@ -6,17 +7,23 @@ import ResetPasswordContainer from '@/components/auth/ResetPasswordContainer';
import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer'; import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer';
import NotFound from '@/components/screens/NotFound'; 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'}> <div className={'pt-8 xl:pt-32'}>
<Switch location={location}> <Switch location={location}>
<Route path={`${match.path}/login`} component={LoginContainer} exact/> <Route path={`${match.path}/login`} component={LoginContainer} exact/>
<Route path={`${match.path}/login/checkpoint`} component={LoginCheckpointContainer}/> <Route path={`${match.path}/login/checkpoint`} component={LoginCheckpointContainer}/>
<Route path={`${match.path}/password`} component={ForgotPasswordContainer} exact/> <Route path={`${match.path}/password`} component={ForgotPasswordContainer} exact/>
<Route path={`${match.path}/password/reset/:token`} component={ResetPasswordContainer}/> <Route path={`${match.path}/password/reset/:token`} component={ResetPasswordContainer}/>
<Route path={`${match.path}/checkpoint`}/> <Route path={`${match.path}/checkpoint`} />
<Route path={'*'}> <Route path={'*'}>
<NotFound onBack={() => history.push('/auth/login')}/> <NotFound onBack={() => history.push('/auth/login')} />
</Route> </Route>
</Switch> </Switch>
</div> </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 { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom';
import AccountOverviewContainer from '@/components/dashboard/AccountOverviewContainer'; import AccountOverviewContainer from '@/components/dashboard/AccountOverviewContainer';
import NavigationBar from '@/components/NavigationBar'; import NavigationBar from '@/components/NavigationBar';
@ -8,9 +9,14 @@ import NotFound from '@/components/screens/NotFound';
import TransitionRouter from '@/TransitionRouter'; import TransitionRouter from '@/TransitionRouter';
import SubNavigation from '@/components/elements/SubNavigation'; import SubNavigation from '@/components/elements/SubNavigation';
export default ({ location }: RouteComponentProps) => ( export default ({ location }: RouteComponentProps) => {
useEffect(() => {
ReactGA.pageview(location.pathname);
}, [ location.pathname ]);
return (
<> <>
<NavigationBar/> <NavigationBar />
{location.pathname.startsWith('/account') && {location.pathname.startsWith('/account') &&
<SubNavigation> <SubNavigation>
<div> <div>
@ -21,11 +27,12 @@ export default ({ location }: RouteComponentProps) => (
} }
<TransitionRouter> <TransitionRouter>
<Switch location={location}> <Switch location={location}>
<Route path={'/'} component={DashboardContainer} exact/> <Route path={'/'} component={DashboardContainer} exact />
<Route path={'/account'} component={AccountOverviewContainer} exact/> <Route path={'/account'} component={AccountOverviewContainer} exact/>
<Route path={'/account/api'} component={AccountApiContainer} exact/> <Route path={'/account/api'} component={AccountApiContainer} exact/>
<Route path={'*'} component={NotFound}/> <Route path={'*'} component={NotFound} />
</Switch> </Switch>
</TransitionRouter> </TransitionRouter>
</> </>
); );
};

View file

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

View file

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

View file

@ -7,6 +7,7 @@ export interface SiteSettings {
enabled: boolean; enabled: boolean;
siteKey: string; siteKey: string;
}; };
analytics: string;
} }
export interface SettingsStore { 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> <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> </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"> <div class="form-group col-md-4">
<label class="control-label">Require 2-Factor Authentication</label> <label class="control-label">Require 2-Factor Authentication</label>
<div> <div>

View file

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

View file

@ -1013,9 +1013,10 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react-google-recaptcha@^1.1.1": "@types/react-helmet@^6.0.0":
version "1.1.1" version "6.0.0"
resolved "https://registry.yarnpkg.com/@types/react-google-recaptcha/-/react-google-recaptcha-1.1.1.tgz#7dd2a4dd15d38d8059a2753cd4a7e3485c9bb3ea" resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.0.0.tgz#5b74e44a12662ffb12d1c97ee702cf4e220958cf"
integrity sha512-NBMPAxgjpaMooXa51cU1BTgrX6T+hQbMiLm77JhBbfOzPQea3RB5rNpPOD5xGWHIVpGXHd59cltEzIq0qglGcQ==
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
@ -5564,11 +5565,16 @@ react-fast-compare@^2.0.1:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" 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" version "3.2.0"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== 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: react-google-recaptcha@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-2.0.1.tgz#3276b29659493f7ca2a5b7739f6c239293cdf1d8" 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" prop-types "^15.5.0"
react-async-script "^1.1.1" 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: react-hot-loader@^4.12.21:
version "4.12.21" version "4.12.21"
resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.21.tgz#332e830801fb33024b5a147d6b13417f491eb975" 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-invariant "^1.0.2"
tiny-warning "^1.0.0" 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: react-transition-group@^4.4.1:
version "4.4.1" version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" 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: dependencies:
picomatch "^2.2.1" 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: reduce-css-calc@^2.1.6:
version "2.1.7" version "2.1.7"
resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.7.tgz#1ace2e02c286d78abcd01fd92bfe8097ab0602c2" resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.7.tgz#1ace2e02c286d78abcd01fd92bfe8097ab0602c2"