mirror of
				https://github.com/pelican-dev/panel.git
				synced 2025-11-04 10:56:52 +01:00 
			
		
		
		
	
						commit
						d3a544ac5d
					
				
							
								
								
									
										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
 | 
				
			|||||||
 | 
					
 | 
				
			||||||

 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 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),
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
 | 
				
			|||||||
@ -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',
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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') ?? '',
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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',
 | 
				
			||||||
 | 
				
			|||||||
@ -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) {
 | 
				
			||||||
 | 
				
			|||||||
@ -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.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
 | 
				
			|||||||
@ -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),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -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),
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /*
 | 
					    /*
 | 
				
			||||||
 | 
				
			|||||||
@ -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",
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -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),
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
				
			|||||||
@ -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/>
 | 
				
			||||||
 | 
				
			|||||||
@ -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'}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,105 +1,39 @@
 | 
				
			|||||||
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 })
 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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)
 | 
					 | 
				
			||||||
            .then(response => {
 | 
					            .then(response => {
 | 
				
			||||||
                if (response.complete) {
 | 
					                if (response.complete) {
 | 
				
			||||||
                    // @ts-ignore
 | 
					                    // @ts-ignore
 | 
				
			||||||
@ -107,26 +41,75 @@ const EnhancedForm = withFormik<OwnProps, LoginData>({
 | 
				
			|||||||
                    return;
 | 
					                    return;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                props.history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
 | 
					                history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
            .catch(error => {
 | 
					            .catch(error => {
 | 
				
			||||||
                console.error(error);
 | 
					                console.error(error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                setSubmitting(false);
 | 
					                setSubmitting(false);
 | 
				
			||||||
                setFieldValue('recaptchaData', null);
 | 
					                clearAndAddHttpError({ error });
 | 
				
			||||||
                props.addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
 | 
					 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
    },
 | 
					    };
 | 
				
			||||||
})(LoginContainer);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default (props: RouteComponentProps) => {
 | 
					 | 
				
			||||||
    const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <EnhancedForm
 | 
					        <Formik
 | 
				
			||||||
            {...props}
 | 
					            onSubmit={onSubmit}
 | 
				
			||||||
            addFlash={addFlash}
 | 
					            initialValues={{ username: '', password: '' }}
 | 
				
			||||||
            clearFlashes={clearFlashes}
 | 
					            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 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`}>
 | 
				
			||||||
 | 
				
			|||||||
@ -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/>
 | 
				
			||||||
 | 
				
			|||||||
@ -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`}>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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 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`}>
 | 
				
			||||||
 | 
				
			|||||||
@ -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 (
 | 
				
			||||||
 | 
				
			|||||||
@ -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>
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
				
			|||||||
@ -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)}>
 | 
				
			||||||
 | 
				
			|||||||
@ -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/>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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 { 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);
 | 
				
			||||||
 | 
				
			|||||||
@ -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/>
 | 
				
			||||||
 | 
				
			|||||||
@ -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}/>
 | 
				
			||||||
 | 
				
			|||||||
@ -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'}
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -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/>
 | 
				
			||||||
                :
 | 
					                :
 | 
				
			||||||
 | 
				
			|||||||
@ -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>
 | 
				
			||||||
 | 
				
			|||||||
@ -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/>
 | 
				
			||||||
 | 
				
			|||||||
@ -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`}>
 | 
				
			||||||
 | 
				
			|||||||
@ -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(() => {
 | 
				
			||||||
        setFieldValue('payload', action === 'power' ? 'start' : '');
 | 
					        if (action !== initialValues.action) {
 | 
				
			||||||
        setFieldTouched('payload', false);
 | 
					            setFieldValue('payload', action === 'power' ? 'start' : '');
 | 
				
			||||||
 | 
					            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>
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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`}>
 | 
				
			||||||
 | 
				
			|||||||
@ -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>
 | 
				
			||||||
            <button
 | 
					            <Can action={'user.update'}>
 | 
				
			||||||
                type={'button'}
 | 
					                {subuser.uuid !== uuid &&
 | 
				
			||||||
                aria-label={'Edit subuser'}
 | 
					                <button
 | 
				
			||||||
                css={[
 | 
					                    type={'button'}
 | 
				
			||||||
                    tw`block text-sm p-2 text-neutral-500 hover:text-neutral-100 transition-colors duration-150 mx-4`,
 | 
					                    aria-label={'Edit subuser'}
 | 
				
			||||||
                    subuser.uuid === uuid ? tw`hidden` : undefined,
 | 
					                    css={tw`block text-sm p-2 text-neutral-500 hover:text-neutral-100 transition-colors duration-150 mx-4`}
 | 
				
			||||||
                ]}
 | 
					                    onClick={() => setVisible(true)}
 | 
				
			||||||
                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>
 | 
				
			||||||
 | 
				
			|||||||
@ -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`}>
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -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) => {
 | 
				
			||||||
    <div className={'pt-8 xl:pt-32'}>
 | 
					    useEffect(() => {
 | 
				
			||||||
        <Switch location={location}>
 | 
					        ReactGA.pageview(location.pathname);
 | 
				
			||||||
            <Route path={`${match.path}/login`} component={LoginContainer} exact/>
 | 
					    }, [ location.pathname ]);
 | 
				
			||||||
            <Route path={`${match.path}/login/checkpoint`} component={LoginCheckpointContainer}/>
 | 
					
 | 
				
			||||||
            <Route path={`${match.path}/password`} component={ForgotPasswordContainer} exact/>
 | 
					    return (
 | 
				
			||||||
            <Route path={`${match.path}/password/reset/:token`} component={ResetPasswordContainer}/>
 | 
					        <div className={'pt-8 xl:pt-32'}>
 | 
				
			||||||
            <Route path={`${match.path}/checkpoint`}/>
 | 
					            <Switch location={location}>
 | 
				
			||||||
            <Route path={'*'}>
 | 
					                <Route path={`${match.path}/login`} component={LoginContainer} exact/>
 | 
				
			||||||
                <NotFound onBack={() => history.push('/auth/login')}/>
 | 
					                <Route path={`${match.path}/login/checkpoint`} component={LoginCheckpointContainer}/>
 | 
				
			||||||
            </Route>
 | 
					                <Route path={`${match.path}/password`} component={ForgotPasswordContainer} exact/>
 | 
				
			||||||
        </Switch>
 | 
					                <Route path={`${match.path}/password/reset/:token`} component={ResetPasswordContainer}/>
 | 
				
			||||||
    </div>
 | 
					                <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 { 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,24 +9,30 @@ 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(() => {
 | 
				
			||||||
        <NavigationBar/>
 | 
					        ReactGA.pageview(location.pathname);
 | 
				
			||||||
        {location.pathname.startsWith('/account') &&
 | 
					    }, [ location.pathname ]);
 | 
				
			||||||
        <SubNavigation>
 | 
					
 | 
				
			||||||
            <div>
 | 
					    return (
 | 
				
			||||||
                <NavLink to={'/account'} exact>Settings</NavLink>
 | 
					        <>
 | 
				
			||||||
                <NavLink to={'/account/api'}>API Credentials</NavLink>
 | 
					            <NavigationBar />
 | 
				
			||||||
            </div>
 | 
					            {location.pathname.startsWith('/account') &&
 | 
				
			||||||
        </SubNavigation>
 | 
					                <SubNavigation>
 | 
				
			||||||
        }
 | 
					                    <div>
 | 
				
			||||||
        <TransitionRouter>
 | 
					                        <NavLink to={'/account'} exact>Settings</NavLink>
 | 
				
			||||||
            <Switch location={location}>
 | 
					                        <NavLink to={'/account/api'}>API Credentials</NavLink>
 | 
				
			||||||
                <Route path={'/'} component={DashboardContainer} exact/>
 | 
					                    </div>
 | 
				
			||||||
                <Route path={'/account'} component={AccountOverviewContainer} exact/>
 | 
					                </SubNavigation>
 | 
				
			||||||
                <Route path={'/account/api'} component={AccountApiContainer} exact/>
 | 
					            }
 | 
				
			||||||
                <Route path={'*'} component={NotFound}/>
 | 
					            <TransitionRouter>
 | 
				
			||||||
            </Switch>
 | 
					                <Switch location={location}>
 | 
				
			||||||
        </TransitionRouter>
 | 
					                    <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 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/>
 | 
				
			||||||
 | 
				
			|||||||
@ -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>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -7,6 +7,7 @@ export interface SiteSettings {
 | 
				
			|||||||
        enabled: boolean;
 | 
					        enabled: boolean;
 | 
				
			||||||
        siteKey: string;
 | 
					        siteKey: string;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					    analytics: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface SettingsStore {
 | 
					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>
 | 
					                                    <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>
 | 
				
			||||||
 | 
				
			|||||||
@ -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');
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										34
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								yarn.lock
									
									
									
									
									
								
							@ -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"
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user