Merge branch 'develop' into feature/react-admin

This commit is contained in:
Matthew Penner 2021-03-21 15:49:41 -06:00
commit 49de31bf4c
24 changed files with 169 additions and 32 deletions

View file

@ -31,7 +31,7 @@ I would like to extend my sincere thanks to the following sponsors for helping f
| [**DeinServerHost**](https://deinserverhost.de/) | DeinServerHost offers Dedicated, vps and Gameservers for many popular Games like Minecraft and Rust in Germany since 2013. |
| [**HostBend**](https://hostbend.com/) | HostBend offers a variety of solutions for developers, students, and others who have a tight budget but don't want to compromise quality and support. |
| [**Capitol Hosting Solutions**](https://capitolsolutions.cloud/) | CHS is *the* budget friendly hosting company for Australian and American gamers, offering a variety of plans from Web Hosting to Game Servers; Custom Solutions too! |
| [**ByteAnia**](https://ByteAnia.com/) | ByteAnia offers the best performing and most affordable **Ryzen 5000 Series hosting** on the market for *unbeatable prices*! |
| [**ByteAnia**](https://byteania.com/?utm_source=pterodactyl) | ByteAnia offers the best performing and most affordable **Ryzen 5000 Series hosting** on the market for *unbeatable prices*! |
## Documentation
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)

View file

@ -6,4 +6,11 @@ use Pterodactyl\Exceptions\DisplayException;
class TwoFactorAuthenticationTokenInvalid extends DisplayException
{
/**
* TwoFactorAuthenticationTokenInvalid constructor.
*/
public function __construct()
{
parent::__construct('The provided two-factor authentication token was not valid.');
}
}

View file

@ -195,7 +195,7 @@ class BackupController extends ClientApiController
// actions against it via the Panel API.
$server->update(['status' => Server::STATUS_RESTORING_BACKUP]);
$this->repository->setServer($server)->restore($backup, $url ?? null, $request->input('truncate') === 'true');
$this->repository->setServer($server)->restore($backup, $url ?? null, $request->input('truncate'));
});
return $this->returnNoContent();

View file

@ -2,7 +2,9 @@
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
use Carbon\Carbon;
use Pterodactyl\Models\Server;
use Illuminate\Cache\Repository;
use Pterodactyl\Transformers\Api\Client\StatsTransformer;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
@ -10,27 +12,35 @@ use Pterodactyl\Http\Requests\Api\Client\Servers\GetServerRequest;
class ResourceUtilizationController extends ClientApiController
{
private Repository $cache;
private DaemonServerRepository $repository;
/**
* ResourceUtilizationController constructor.
*/
public function __construct(DaemonServerRepository $repository)
public function __construct(Repository $cache, DaemonServerRepository $repository)
{
parent::__construct();
$this->cache = $cache;
$this->repository = $repository;
}
/**
* Return the current resource utilization for a server.
* Return the current resource utilization for a server. This value is cached for up to
* 20 seconds at a time to ensure that repeated requests to this endpoint do not cause
* a flood of unnecessary API calls.
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function __invoke(GetServerRequest $request, Server $server): array
{
$stats = $this->repository->setServer($server)->getDetails();
$stats = $this->cache
->tags(['resources'])
->remember($server->uuid, Carbon::now()->addSeconds(20), function () use ($server) {
return $this->repository->setServer($server)->getDetails();
});
return $this->fractal->item($stats)
->transformWith($this->getTransformer(StatsTransformer::class))

View file

@ -57,7 +57,14 @@ class TwoFactorController extends ClientApiController
/**
* Updates a user's account to have two-factor enabled.
*
* @return \Illuminate\Http\JsonResponse
*
* @throws \Throwable
* @throws \Illuminate\Validation\ValidationException
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
* @throws \Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid
*/
public function store(Request $request): JsonResponse
{

View file

@ -122,13 +122,14 @@ class Schedule extends Model
* Returns the schedule's execution crontab entry as a string.
*
* @return \Carbon\CarbonImmutable
* @throws \Exception
*/
public function getNextRunDate()
{
$formatted = sprintf('%s %s %s %s %s', $this->cron_minute, $this->cron_hour, $this->cron_day_of_month, $this->cron_month, $this->cron_day_of_week);
return CarbonImmutable::createFromTimestamp(
CronExpression::factory($formatted)->getNextRunDate()->getTimestamp()
(new CronExpression($formatted))->getNextRunDate()->getTimestamp()
);
}

View file

@ -5,10 +5,12 @@ namespace Pterodactyl\Services\Servers;
use Illuminate\Support\Arr;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Allocation;
use Illuminate\Support\Facades\Log;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\DisplayException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class BuildModificationService
{
@ -78,10 +80,18 @@ class BuildModificationService
$updateData = $this->structureService->handle($server);
// Because Wings always fetches an updated configuration from the Panel when booting
// a server this type of exception can be safely "ignored" and just written to the logs.
// Ideally this request succeedes so we can apply resource modifications on the fly
// but if it fails it isn't the end of the world.
if (!empty($updateData['build'])) {
try {
$this->daemonServerRepository->setServer($server)->update([
'build' => $updateData['build'],
]);
} catch (DaemonConnectionException $exception) {
Log::warning($exception, ['server_id' => $server->id]);
}
}
$this->connection->commit();

View file

@ -74,7 +74,7 @@ class ToggleTwoFactorService
$isValidToken = $this->google2FA->verifyKey($secret, $token, config()->get('pterodactyl.auth.2fa.window'));
if (!$isValidToken) {
throw new TwoFactorAuthenticationTokenInvalid('The token provided is not valid.');
throw new TwoFactorAuthenticationTokenInvalid();
}
return $this->connection->transaction(function () use ($user, $toggleState) {
@ -94,6 +94,9 @@ class ToggleTwoFactorService
$inserts[] = [
'user_id' => $user->id,
'token' => password_hash($token, PASSWORD_DEFAULT),
// insert() won't actually set the time on the models, so make sure we do this
// manually here.
'created_at' => Carbon::now(),
];
$tokens[] = $token;

View file

@ -3,7 +3,7 @@
"meta": {
"version": "PTDL_v1"
},
"exported_at": "2020-11-03T04:22:56+00:00",
"exported_at": "2021-03-21T17:52:00+00:00",
"name": "Bungeecord",
"author": "support@pterodactyl.io",
"description": "For a long time, Minecraft server owners have had a dream that encompasses a free, easy, and reliable way to connect multiple Minecraft servers together. BungeeCord is the answer to said dream. Whether you are a small server wishing to string multiple game-modes together, or the owner of the ShotBow Network, BungeeCord is the ideal solution for you. With the help of BungeeCord, you will be able to unlock your community's full potential.",
@ -11,7 +11,7 @@
"images": ["quay.io\/pterodactyl\/core:java", "quay.io\/pterodactyl\/core:java-11"],
"startup": "java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar {{SERVER_JARFILE}}",
"config": {
"files": "{\r\n \"config.yml\": {\r\n \"parser\": \"yaml\",\r\n \"find\": {\r\n \"listeners[0].query_enabled\": true,\r\n \"listeners[0].query_port\": \"{{server.build.default.port}}\",\r\n \"listeners[0].host\": \"0.0.0.0:{{server.build.default.port}}\",\r\n \"servers.*.address\": {\r\n \"regex:^(127\\\\.0\\\\.0\\\\.1|localhost)(:\\\\d{1,5})?$\": \"{{config.docker.interface}}$2\"\r\n }\r\n }\r\n }\r\n}",
"files": "{\r\n \"config.yml\": {\r\n \"parser\": \"yaml\",\r\n \"find\": {\r\n \"listeners[0].query_port\": \"{{server.build.default.port}}\",\r\n \"listeners[0].host\": \"0.0.0.0:{{server.build.default.port}}\",\r\n \"servers.*.address\": {\r\n \"regex:^(127\\\\.0\\\\.0\\\\.1|localhost)(:\\\\d{1,5})?$\": \"{{config.docker.interface}}$2\"\r\n }\r\n }\r\n }\r\n}",
"startup": "{\r\n \"done\": \"Listening on \",\r\n \"userInteraction\": [\r\n \"Listening on \/0.0.0.0:25577\"\r\n ]\r\n}",
"logs": "{\r\n \"custom\": false,\r\n \"location\": \"proxy.log.0\"\r\n}",
"stop": "end"

View file

@ -4,7 +4,7 @@
"version": "PTDL_v1",
"update_url": null
},
"exported_at": "2021-02-22T19:08:49+04:00",
"exported_at": "2021-03-15T18:04:38+02:00",
"name": "Forge Minecraft",
"author": "support@pterodactyl.io",
"description": "Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.",
@ -15,6 +15,7 @@
"quay.io\/pterodactyl\/core:java",
"quay.io\/pterodactyl\/core:java-11"
],
"file_denylist": [],
"startup": "java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar {{SERVER_JARFILE}}",
"config": {
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"enable-query\": \"true\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}",
@ -24,7 +25,7 @@
},
"scripts": {
"installation": {
"script": "#!\/bin\/bash\r\n# Forge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt update\r\napt install -y curl jq\r\n\r\n#Go into main direction\r\nif [ ! -d \/mnt\/server ]; then\r\n mkdir \/mnt\/server\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\nif [ ! -z ${FORGE_VERSION} ]; then\r\n DOWNLOAD_LINK=https:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/${FORGE_VERSION}\/forge-${FORGE_VERSION}\r\n FORGE_JAR=forge-${FORGE_VERSION}.jar\r\nelse\r\n JSON_DATA=$(curl -sSL https:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/promotions_slim.json)\r\n\r\n if [ \"${MC_VERSION}\" == \"latest\" ] || [ \"${MC_VERSION}\" == \"\" ] ; then\r\n echo -e \"getting latest recommended version of forge.\"\r\n MC_VERSION=$(echo -e ${JSON_DATA} | jq -r '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains(\"recommended\")) | split(\"-\")[0]' | sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n | tail -1)\r\n \tBUILD_TYPE=recommended\r\n fi\r\n\r\n if [ \"${BUILD_TYPE}\" != \"recommended\" ] && [ \"${BUILD_TYPE}\" != \"latest\" ]; then\r\n BUILD_TYPE=recommended\r\n fi\r\n\r\n echo -e \"minecraft version: ${MC_VERSION}\"\r\n echo -e \"build type: ${BUILD_TYPE}\"\r\n\r\n ## some variables for getting versions and things\r\n FILE_SITE=$(echo -e ${JSON_DATA} | jq -r '.homepage' | sed \"s\/http:\/https:\/g\")\r\n VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION \"${MC_VERSION}\" --arg BUILD_TYPE \"${BUILD_TYPE}\" '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains($BUILD_TYPE))')\r\n\r\n ## locating the forge version\r\n if [ \"${VERSION_KEY}\" == \"\" ] && [ \"${BUILD_TYPE}\" == \"recommended\" ]; then\r\n echo -e \"dropping back to latest from recommended due to there not being a recommended version of forge for the mc version requested.\"\r\n VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION \"${MC_VERSION}\" '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains(\"recommended\"))')\r\n fi\r\n\r\n ## Error if the mc version set wasn't valid.\r\n if [ \"${VERSION_KEY}\" == \"\" ] || [ \"${VERSION_KEY}\" == \"null\" ]; then\r\n \techo -e \"The install failed because there is no valid version of forge for the version on minecraft selected.\"\r\n \texit 1\r\n fi\r\n\r\n FORGE_VERSION=$(echo -e ${JSON_DATA} | jq -r --arg VERSION_KEY \"$VERSION_KEY\" '.promos | .[$VERSION_KEY]')\r\n\r\n if [ \"${MC_VERSION}\" == \"1.7.10\" ] || [ \"${MC_VERSION}\" == \"1.8.9\" ]; then\r\n DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}\/forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}.jar\r\n if [ \"${MC_VERSION}\" == \"1.7.10\" ]; then\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}-universal.jar\r\n fi\r\n else\r\n DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}\/forge-${MC_VERSION}-${FORGE_VERSION}\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}.jar\r\n fi\r\nfi\r\n\r\n\r\n#Adding .jar when not eding by SERVER_JARFILE\r\nif [[ ! $SERVER_JARFILE = *\\.jar ]]; then\r\n SERVER_JARFILE=\"$SERVER_JARFILE.jar\"\r\nfi\r\n\r\n#Downloading jars\r\necho -e \"Downloading forge version ${FORGE_VERSION}\"\r\necho -e \"Download link is ${DOWNLOAD_LINK}\"\r\nif [ ! -z \"${DOWNLOAD_LINK}\" ]; then \r\n if curl --output \/dev\/null --silent --head --fail ${DOWNLOAD_LINK}-installer.jar; then\r\n echo -e \"installer jar download link is valid.\"\r\n else\r\n echo -e \"link is invalid closing out\"\r\n exit 2\r\n fi\r\nelse\r\n echo -e \"no download link closing out\"\r\n exit 3\r\nfi\r\n\r\ncurl -s -o installer.jar -sS ${DOWNLOAD_LINK}-installer.jar\r\n\r\n#Checking if downloaded jars exist\r\nif [ ! -f .\/installer.jar ]; then\r\n echo \"!!! Error by downloading forge version ${FORGE_VERSION} !!!\"\r\n exit\r\nfi\r\n\r\n#Installing server\r\necho -e \"Installing forge server.\\n\"\r\njava -jar installer.jar --installServer || { echo -e \"install failed\"; exit 4; }\r\n\r\nmv $FORGE_JAR $SERVER_JARFILE\r\n\r\n#Deleting installer.jar\r\necho -e \"Deleting installer.jar file.\\n\"\r\nrm -rf installer.jar",
"script": "#!\/bin\/bash\r\n# Forge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt update\r\napt install -y curl jq\r\n\r\n#Go into main direction\r\nif [ ! -d \/mnt\/server ]; then\r\n mkdir \/mnt\/server\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\nif [ ! -z ${FORGE_VERSION} ]; then\r\n DOWNLOAD_LINK=https:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/${FORGE_VERSION}\/forge-${FORGE_VERSION}\r\n FORGE_JAR=forge-${FORGE_VERSION}*.jar\r\nelse\r\n JSON_DATA=$(curl -sSL https:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/promotions_slim.json)\r\n\r\n if [ \"${MC_VERSION}\" == \"latest\" ] || [ \"${MC_VERSION}\" == \"\" ] ; then\r\n echo -e \"getting latest recommended version of forge.\"\r\n MC_VERSION=$(echo -e ${JSON_DATA} | jq -r '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains(\"recommended\")) | split(\"-\")[0]' | sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n | tail -1)\r\n \tBUILD_TYPE=recommended\r\n fi\r\n\r\n if [ \"${BUILD_TYPE}\" != \"recommended\" ] && [ \"${BUILD_TYPE}\" != \"latest\" ]; then\r\n BUILD_TYPE=recommended\r\n fi\r\n\r\n echo -e \"minecraft version: ${MC_VERSION}\"\r\n echo -e \"build type: ${BUILD_TYPE}\"\r\n\r\n ## some variables for getting versions and things\r\n FILE_SITE=$(echo -e ${JSON_DATA} | jq -r '.homepage' | sed \"s\/http:\/https:\/g\")\r\n VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION \"${MC_VERSION}\" --arg BUILD_TYPE \"${BUILD_TYPE}\" '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains($BUILD_TYPE))')\r\n\r\n ## locating the forge version\r\n if [ \"${VERSION_KEY}\" == \"\" ] && [ \"${BUILD_TYPE}\" == \"recommended\" ]; then\r\n echo -e \"dropping back to latest from recommended due to there not being a recommended version of forge for the mc version requested.\"\r\n VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION \"${MC_VERSION}\" '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains(\"recommended\"))')\r\n fi\r\n\r\n ## Error if the mc version set wasn't valid.\r\n if [ \"${VERSION_KEY}\" == \"\" ] || [ \"${VERSION_KEY}\" == \"null\" ]; then\r\n \techo -e \"The install failed because there is no valid version of forge for the version on minecraft selected.\"\r\n \texit 1\r\n fi\r\n\r\n FORGE_VERSION=$(echo -e ${JSON_DATA} | jq -r --arg VERSION_KEY \"$VERSION_KEY\" '.promos | .[$VERSION_KEY]')\r\n\r\n if [ \"${MC_VERSION}\" == \"1.7.10\" ] || [ \"${MC_VERSION}\" == \"1.8.9\" ]; then\r\n DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}\/forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}.jar\r\n if [ \"${MC_VERSION}\" == \"1.7.10\" ]; then\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}-universal.jar\r\n fi\r\n else\r\n DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}\/forge-${MC_VERSION}-${FORGE_VERSION}\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}.jar\r\n fi\r\nfi\r\n\r\n\r\n#Adding .jar when not eding by SERVER_JARFILE\r\nif [[ ! $SERVER_JARFILE = *\\.jar ]]; then\r\n SERVER_JARFILE=\"$SERVER_JARFILE.jar\"\r\nfi\r\n\r\n#Downloading jars\r\necho -e \"Downloading forge version ${FORGE_VERSION}\"\r\necho -e \"Download link is ${DOWNLOAD_LINK}\"\r\nif [ ! -z \"${DOWNLOAD_LINK}\" ]; then \r\n if curl --output \/dev\/null --silent --head --fail ${DOWNLOAD_LINK}-installer.jar; then\r\n echo -e \"installer jar download link is valid.\"\r\n else\r\n echo -e \"link is invalid closing out\"\r\n exit 2\r\n fi\r\nelse\r\n echo -e \"no download link closing out\"\r\n exit 3\r\nfi\r\n\r\ncurl -s -o installer.jar -sS ${DOWNLOAD_LINK}-installer.jar\r\n\r\n#Checking if downloaded jars exist\r\nif [ ! -f .\/installer.jar ]; then\r\n echo \"!!! Error by downloading forge version ${FORGE_VERSION} !!!\"\r\n exit\r\nfi\r\n\r\n#Installing server\r\necho -e \"Installing forge server.\\n\"\r\njava -jar installer.jar --installServer || { echo -e \"install failed\"; exit 4; }\r\n\r\nmv $FORGE_JAR $SERVER_JARFILE\r\n\r\n#Deleting installer.jar\r\necho -e \"Deleting installer.jar file.\\n\"\r\nrm -rf installer.jar",
"container": "openjdk:8-jdk-slim",
"entrypoint": "bash"
}

View file

@ -38,7 +38,7 @@
},
{
"name": "Server Version",
"description": "The version of Minecraft Vanilla to install. Use \"latest\" to install the latest version, or use \"snapshot\" to install the latest snapshot.",
"description": "The version of Minecraft Vanilla to install. Use \"latest\" to install the latest version, or use \"snapshot\" to install the latest snapshot. Go to Settings > Reinstall Server to apply.",
"env_variable": "VANILLA_VERSION",
"default_value": "latest",
"user_viewable": true,

View file

@ -0,0 +1,31 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ForceCronMonthFieldToHaveValueIfMissing extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('schedules', function (Blueprint $table) {
DB::update("UPDATE `schedules` SET `cron_month` = '*' WHERE `cron_month` = ''");
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// No down function.
}
}

View file

@ -59,7 +59,7 @@ services:
- "/srv/pterodactyl/var/:/app/var/"
- "/srv/pterodactyl/nginx/:/etc/nginx/conf.d/"
- "/srv/pterodactyl/certs/:/etc/letsencrypt/"
- "/srv/pterodactyl/logs/:/var/log/"
- "/srv/pterodactyl/logs/:/app/storage/logs"
environment:
<<: *panel-environment
<<: *mail-environment

View file

@ -1,5 +1,7 @@
import http from '@/api/http';
export const restoreServerBackup = async (uuid: string, backup: string): Promise<void> => {
await http.post(`/api/client/servers/${uuid}/backups/${backup}/restore`);
export const restoreServerBackup = async (uuid: string, backup: string, truncate?: boolean): Promise<void> => {
await http.post(`/api/client/servers/${uuid}/backups/${backup}/restore`, {
truncate,
});
};

View file

@ -27,7 +27,7 @@ const Container = styled.div`
`;
export default () => {
const state = useLocation<{ twoFactorRedirect: boolean }>().state;
const { state } = useLocation<undefined | { twoFactorRedirect?: boolean }>();
return (
<PageContentBlock title={'Account Overview'}>

View file

@ -12,10 +12,14 @@ import tw from 'twin.macro';
import useSWR from 'swr';
import { PaginatedResult } from '@/api/http';
import Pagination from '@/components/elements/Pagination';
import { useLocation } from 'react-router-dom';
export default () => {
const { search } = useLocation();
const defaultPage = Number(new URLSearchParams(search).get('page') || '1');
const [ page, setPage ] = useState((!isNaN(defaultPage) && defaultPage > 0) ? defaultPage : 1);
const { clearFlashes, clearAndAddHttpError } = useFlash();
const [ page, setPage ] = useState(1);
const uuid = useStoreState(state => state.user.data!.uuid);
const rootAdmin = useStoreState(state => state.user.data!.rootAdmin);
const [ showOnlyAdmin, setShowOnlyAdmin ] = usePersistedState(`${uuid}:show_all_servers`, false);
@ -25,6 +29,20 @@ export default () => {
() => getServers({ page, type: showOnlyAdmin ? 'admin' : undefined }),
);
useEffect(() => {
if (!servers) return;
if (servers.pagination.currentPage > 1 && !servers.items.length) {
setPage(1);
}
}, [ servers?.pagination.currentPage ]);
useEffect(() => {
// Don't use react-router to handle changing this part of the URL, otherwise it
// triggers a needless re-render. We just want to track this in the URL incase the
// user refreshes the page.
window.history.replaceState(null, document.title, `/${page <= 1 ? '' : `?page=${page}`}`);
}, [ page ]);
useEffect(() => {
if (error) clearAndAddHttpError({ key: 'dashboard', error });
if (!error) clearFlashes('dashboard');

View file

@ -59,7 +59,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
getStats().then(() => {
// @ts-ignore
interval.current = setInterval(() => getStats(), 20000);
interval.current = setInterval(() => getStats(), 30000);
});
return () => {

View file

@ -78,7 +78,7 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
<h2 css={tw`text-2xl mb-4`}>Two-factor authentication enabled</h2>
<p css={tw`text-neutral-300`}>
Two-factor authentication has been enabled on your account. Should you loose access to
this device you&apos;ll need to use on of the codes displayed below in order to access your
this device you&apos;ll need to use one of the codes displayed below in order to access your
account.
</p>
<p css={tw`text-neutral-300 mt-4`}>

View file

@ -146,10 +146,10 @@ export default () => {
// Add support for capturing keys
terminal.attachCustomKeyEventHandler((e: KeyboardEvent) => {
if (e.metaKey && e.key === 'c') {
if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
document.execCommand('copy');
return false;
} else if (e.metaKey && e.key === 'f') {
} else if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault();
searchBar.show();
return false;

View file

@ -25,6 +25,7 @@ export default ({ backup }: Props) => {
const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState);
const [ modal, setModal ] = useState('');
const [ loading, setLoading ] = useState(false);
const [ truncate, setTruncate ] = useState(false);
const { clearFlashes, clearAndAddHttpError } = useFlash();
const { mutate } = getServerBackups();
@ -62,7 +63,7 @@ export default ({ backup }: Props) => {
const doRestorationAction = () => {
setLoading(true);
clearFlashes('backups');
restoreServerBackup(uuid, backup.uuid)
restoreServerBackup(uuid, backup.uuid, truncate)
.then(() => setServerFromState(s => ({
...s,
status: 'restoring_backup',
@ -108,6 +109,8 @@ export default ({ backup }: Props) => {
css={tw`text-red-500! w-5! h-5! mr-2`}
id={'restore_truncate'}
value={'true'}
checked={truncate}
onChange={() => setTruncate(s => !s)}
/>
Remove all files and folders before restoring this backup.
</label>

View file

@ -78,7 +78,7 @@ const StartupContainer = () => {
/>
:
<ServerContentBlock title={'Startup Settings'} showFlashKey={'startup:image'}>
<div css={tw`flex`}>
<div css={tw`md:flex`}>
<TitledGreyBox title={'Startup Command'} css={tw`flex-1`}>
<div css={tw`px-1 py-2`}>
<p css={tw`font-mono bg-neutral-900 rounded py-2 px-4`}>
@ -86,7 +86,7 @@ const StartupContainer = () => {
</p>
</div>
</TitledGreyBox>
<TitledGreyBox title={'Docker Image'} css={tw`flex-1 lg:flex-none lg:w-1/3 ml-10`}>
<TitledGreyBox title={'Docker Image'} css={tw`flex-1 lg:flex-none lg:w-1/3 mt-8 md:mt-0 md:ml-10`}>
{data.dockerImages.length > 1 && !isCustomImage ?
<>
<InputSpinner visible={loading}>

View file

@ -21,10 +21,18 @@ export default ({ location }: RouteComponentProps) => (
}
<TransitionRouter>
<Switch location={location}>
<Route path={'/'} component={DashboardContainer} exact/>
<Route path={'/account'} component={AccountOverviewContainer} exact/>
<Route path={'/account/api'} component={AccountApiContainer} exact/>
<Route path={'*'} component={NotFound}/>
<Route path={'/'} exact>
<DashboardContainer/>
</Route>
<Route path={'/account'} exact>
<AccountOverviewContainer/>
</Route>
<Route path={'/account/api'} exact>
<AccountApiContainer/>
</Route>
<Route path={'*'}>
<NotFound/>
</Route>
</Switch>
</TransitionRouter>
</>

View file

@ -101,6 +101,11 @@ class TwoFactorControllerTest extends ClientApiIntegrationTestCase
$tokens = RecoveryToken::query()->where('user_id', $user->id)->get();
$this->assertCount(10, $tokens);
$this->assertStringStartsWith('$2y$10$', $tokens[0]->token);
// Ensure the recovery tokens that were created include a "created_at" timestamp
// value on them.
//
// @see https://github.com/pterodactyl/panel/issues/3163
$this->assertNotNull($tokens[0]->created_at);
$tokens = $tokens->pluck('token')->toArray();

View file

@ -3,12 +3,17 @@
namespace Pterodactyl\Tests\Integration\Services\Servers;
use Mockery;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Allocation;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\TransferException;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Tests\Integration\IntegrationTestCase;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Services\Servers\BuildModificationService;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class BuildModificationServiceTest extends IntegrationTestCase
{
@ -149,6 +154,30 @@ class BuildModificationServiceTest extends IntegrationTestCase
$this->assertSame(20, $response->allocation_limit);
}
/**
* Test that an exception when connecting to the Wings instance is properly ignored
* when making updates. This allows for a server to be modified even when the Wings
* node is offline.
*/
public function testConnectionExceptionIsIgnoredWhenUpdatingServerSettings()
{
$server = $this->createServerModel();
$this->daemonServerRepository->expects('setServer->update')->andThrows(
new DaemonConnectionException(
new RequestException('Bad request', new Request('GET', '/test'), new Response())
)
);
$response = $this->getService()->handle($server, ['memory' => 256, 'disk' => 10240]);
$this->assertInstanceOf(Server::class, $response);
$this->assertSame(256, $response->memory);
$this->assertSame(10240, $response->disk);
$this->assertDatabaseHas('servers', ['id' => $response->id, 'memory' => 256, 'disk' => 10240]);
}
/**
* Test that no exception is thrown if we are only removing an allocation.
*/
@ -215,7 +244,9 @@ class BuildModificationServiceTest extends IntegrationTestCase
/**
* Test that any changes we made to the server or allocations are rolled back if there is an
* exception while performing any action.
* exception while performing any action. This is different than the connection exception
* test which should properly ignore connection issues. We want any other type of exception
* to properly be thrown back to the caller.
*/
public function testThatUpdatesAreRolledBackIfExceptionIsEncountered()
{