commit
d3a544ac5d
52 changed files with 498 additions and 239 deletions
23
README.md
23
README.md
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ class AssetComposer
|
|||
'enabled' => config('recaptcha.enabled', false),
|
||||
'siteKey' => config('recaptcha.website_key') ?? '',
|
||||
],
|
||||
'analytics' => config('app.analytics') ?? '',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ class SettingsServiceProvider extends ServiceProvider
|
|||
protected $keys = [
|
||||
'app:name',
|
||||
'app:locale',
|
||||
'app:analytics',
|
||||
'recaptcha:enabled',
|
||||
'recaptcha:secret_key',
|
||||
'recaptcha:website_key',
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
|
||||
/*
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -6,19 +6,19 @@ export default createGlobalStyle`
|
|||
${tw`font-sans bg-neutral-800 text-neutral-200`};
|
||||
letter-spacing: 0.015em;
|
||||
}
|
||||
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
${tw`font-medium tracking-normal font-header`};
|
||||
}
|
||||
|
||||
|
||||
p {
|
||||
${tw`text-neutral-200 leading-snug font-sans`};
|
||||
}
|
||||
|
||||
|
||||
form {
|
||||
${tw`m-0`};
|
||||
}
|
||||
|
||||
|
||||
textarea, select, input, button, button:focus, button:focus-visible {
|
||||
${tw`outline-none`};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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/>
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -1,105 +1,39 @@
|
|||
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);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{ref.current && ref.current.render()}
|
||||
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`} onSubmit={submit}>
|
||||
<Field
|
||||
type={'text'}
|
||||
label={'Username or Email'}
|
||||
id={'username'}
|
||||
name={'username'}
|
||||
light
|
||||
/>
|
||||
<div css={tw`mt-6`}>
|
||||
<Field
|
||||
type={'password'}
|
||||
label={'Password'}
|
||||
id={'password'}
|
||||
name={'password'}
|
||||
light
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`mt-6`}>
|
||||
<Button type={'submit'} size={'xlarge'} isLoading={isSubmitting}>
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
{recaptchaEnabled &&
|
||||
<ReCAPTCHA
|
||||
ref={ref}
|
||||
size={'invisible'}
|
||||
sitekey={siteKey || '_invalid_key'}
|
||||
onChange={token => {
|
||||
ref.current && ref.current.reset();
|
||||
setFieldValue('recaptchaData', token);
|
||||
submitForm();
|
||||
}}
|
||||
onExpired={() => setFieldValue('recaptchaData', null)}
|
||||
/>
|
||||
}
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
<Link
|
||||
to={'/auth/password'}
|
||||
css={tw`text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600`}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
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)
|
||||
login({ ...values, recaptchaData: token })
|
||||
.then(response => {
|
||||
if (response.complete) {
|
||||
// @ts-ignore
|
||||
|
@ -107,26 +41,75 @@ const EnhancedForm = withFormik<OwnProps, LoginData>({
|
|||
return;
|
||||
}
|
||||
|
||||
props.history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
|
||||
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) });
|
||||
clearAndAddHttpError({ error });
|
||||
});
|
||||
},
|
||||
})(LoginContainer);
|
||||
|
||||
export default (props: RouteComponentProps) => {
|
||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
};
|
||||
|
||||
return (
|
||||
<EnhancedForm
|
||||
{...props}
|
||||
addFlash={addFlash}
|
||||
clearFlashes={clearFlashes}
|
||||
/>
|
||||
<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'}
|
||||
id={'username'}
|
||||
name={'username'}
|
||||
light
|
||||
/>
|
||||
<div css={tw`mt-6`}>
|
||||
<Field
|
||||
type={'password'}
|
||||
label={'Password'}
|
||||
id={'password'}
|
||||
name={'password'}
|
||||
light
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`mt-6`}>
|
||||
<Button type={'submit'} size={'xlarge'} isLoading={isSubmitting}>
|
||||
Login
|
||||
</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
|
||||
to={'/auth/password'}
|
||||
css={tw`text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600`}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginContainer;
|
||||
|
|
|
@ -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`}>
|
||||
|
|
|
@ -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/>
|
||||
|
|
|
@ -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`}>
|
||||
|
|
26
resources/scripts/components/server/InstallListener.tsx
Normal file
26
resources/scripts/components/server/InstallListener.tsx
Normal 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;
|
|
@ -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`}>
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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)}>
|
||||
|
|
|
@ -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/>
|
||||
|
|
10
resources/scripts/components/server/events.ts
Normal file
10
resources/scripts/components/server/events.ts
Normal 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',
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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/>
|
||||
|
|
|
@ -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}/>
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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/>
|
||||
:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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/>
|
||||
|
|
|
@ -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`}>
|
||||
|
|
|
@ -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(() => {
|
||||
setFieldValue('payload', action === 'power' ? 'start' : '');
|
||||
setFieldTouched('payload', false);
|
||||
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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`}>
|
||||
|
|
|
@ -51,17 +51,18 @@ export default ({ subuser }: Props) => {
|
|||
</p>
|
||||
<p css={tw`text-2xs text-neutral-500 uppercase`}>Permissions</p>
|
||||
</div>
|
||||
<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,
|
||||
]}
|
||||
onClick={() => setVisible(true)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencilAlt}/>
|
||||
</button>
|
||||
<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`}
|
||||
onClick={() => setVisible(true)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencilAlt}/>
|
||||
</button>
|
||||
}
|
||||
</Can>
|
||||
<Can action={'user.delete'}>
|
||||
<RemoveSubuserButton subuser={subuser}/>
|
||||
</Can>
|
||||
|
|
|
@ -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`}>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,17 +7,23 @@ import ResetPasswordContainer from '@/components/auth/ResetPasswordContainer';
|
|||
import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer';
|
||||
import NotFound from '@/components/screens/NotFound';
|
||||
|
||||
export default ({ location, history, match }: RouteComponentProps) => (
|
||||
<div className={'pt-8 xl:pt-32'}>
|
||||
<Switch location={location}>
|
||||
<Route path={`${match.path}/login`} component={LoginContainer} exact/>
|
||||
<Route path={`${match.path}/login/checkpoint`} component={LoginCheckpointContainer}/>
|
||||
<Route path={`${match.path}/password`} component={ForgotPasswordContainer} exact/>
|
||||
<Route path={`${match.path}/password/reset/:token`} component={ResetPasswordContainer}/>
|
||||
<Route path={`${match.path}/checkpoint`}/>
|
||||
<Route path={'*'}>
|
||||
<NotFound onBack={() => history.push('/auth/login')}/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
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/>
|
||||
<Route path={`${match.path}/login/checkpoint`} component={LoginCheckpointContainer}/>
|
||||
<Route path={`${match.path}/password`} component={ForgotPasswordContainer} exact/>
|
||||
<Route path={`${match.path}/password/reset/:token`} component={ResetPasswordContainer}/>
|
||||
<Route path={`${match.path}/checkpoint`} />
|
||||
<Route path={'*'}>
|
||||
<NotFound onBack={() => history.push('/auth/login')} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,24 +9,30 @@ import NotFound from '@/components/screens/NotFound';
|
|||
import TransitionRouter from '@/TransitionRouter';
|
||||
import SubNavigation from '@/components/elements/SubNavigation';
|
||||
|
||||
export default ({ location }: RouteComponentProps) => (
|
||||
<>
|
||||
<NavigationBar/>
|
||||
{location.pathname.startsWith('/account') &&
|
||||
<SubNavigation>
|
||||
<div>
|
||||
<NavLink to={'/account'} exact>Settings</NavLink>
|
||||
<NavLink to={'/account/api'}>API Credentials</NavLink>
|
||||
</div>
|
||||
</SubNavigation>
|
||||
}
|
||||
<TransitionRouter>
|
||||
<Switch location={location}>
|
||||
<Route path={'/'} component={DashboardContainer} exact/>
|
||||
<Route path={'/account'} component={AccountOverviewContainer} exact/>
|
||||
<Route path={'/account/api'} component={AccountApiContainer} exact/>
|
||||
<Route path={'*'} component={NotFound}/>
|
||||
</Switch>
|
||||
</TransitionRouter>
|
||||
</>
|
||||
);
|
||||
export default ({ location }: RouteComponentProps) => {
|
||||
useEffect(() => {
|
||||
ReactGA.pageview(location.pathname);
|
||||
}, [ location.pathname ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavigationBar />
|
||||
{location.pathname.startsWith('/account') &&
|
||||
<SubNavigation>
|
||||
<div>
|
||||
<NavLink to={'/account'} exact>Settings</NavLink>
|
||||
<NavLink to={'/account/api'}>API Credentials</NavLink>
|
||||
</div>
|
||||
</SubNavigation>
|
||||
}
|
||||
<TransitionRouter>
|
||||
<Switch location={location}>
|
||||
<Route path={'/'} component={DashboardContainer} exact />
|
||||
<Route path={'/account'} component={AccountOverviewContainer} exact/>
|
||||
<Route path={'/account/api'} component={AccountApiContainer} exact/>
|
||||
<Route path={'*'} component={NotFound} />
|
||||
</Switch>
|
||||
</TransitionRouter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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/>
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ export interface SiteSettings {
|
|||
enabled: boolean;
|
||||
siteKey: string;
|
||||
};
|
||||
analytics: string;
|
||||
}
|
||||
|
||||
export interface SettingsStore {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
|
|
34
yarn.lock
34
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue