Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 79 additions & 24 deletions app/Http/Controllers/Admin/ServerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
use App\Settings\LocaleSettings;
use App\Settings\PterodactylSettings;
use App\Classes\PterodactylClient;
use App\Facades\Currency;
use App\Services\CreditService;
use Exception;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
Expand Down Expand Up @@ -154,9 +156,11 @@ public function update(Request $request, Server $server, DiscordSettings $discor
* Remove the specified resource from storage.
*
* @param Server $server
* @param Request $request
* @param DiscordSettings $discord_settings
* @return RedirectResponse|Response
*/
public function destroy(Server $server, DiscordSettings $discord_settings)
public function destroy(Server $server, Request $request, DiscordSettings $discord_settings)
{
$this->checkPermission(self::DELETE_PERMISSION);
try {
Expand All @@ -173,6 +177,17 @@ public function destroy(Server $server, DiscordSettings $discord_settings)
log::debug('Failed to update discord roles' . $e->getMessage());
}

if ($request->has('refund')) {
$user = User::findOrFail($server->user_id);
$credits = (int) round($server->product->price);
app(CreditService::class)->refund($user, $credits);

activity()
->performedOn($server)
->causedBy(Auth::user())
->log("Server credits (" . Currency::formatForDisplay($credits) . ") refunded to user " . $user->name . " during deletion.");
}

// Attempt to remove the server from pterodactyl
$server->delete();

Expand Down Expand Up @@ -236,42 +251,77 @@ public function toggleSuspended(Request $request, Server $server)
public function syncServers()
{
$this->checkPermission(self::WRITE_PERMISSION);
$CPServers = Server::get();
$CPServers = Server::all();

$CPIDArray = [];
$renameCount = 0;
foreach ($CPServers as $CPServer) { //go thru all CP servers and make array with IDs as keys. All values are false.
$recoveredCount = 0;
$deleteCount = 0;

// 1. Handle servers with missing pterodactyl_id (Try to recover via external_id)
foreach ($CPServers->whereNull('pterodactyl_id') as $serverWithoutId) {
try {
$response = $this->pterodactyl->getServerByExternalId($serverWithoutId->id);
if ($response->successful()) {
$attributes = $response->json()['attributes'] ?? null;
if ($attributes && isset($attributes['id'])) {
$serverWithoutId->update([
'pterodactyl_id' => $attributes['id'],
'identifier' => $attributes['identifier'] ?? $serverWithoutId->identifier,
'status' => Server::STATUS_ACTIVE,
]);
$recoveredCount++;
}
}
} catch (Exception $e) {
Log::error("Failed to sync server without ID {$serverWithoutId->id}: " . $e->getMessage());
}
}

// Refresh list after recovery attempts
$CPServers = Server::all();

// 2. Map existing pterodactyl_id for presence check
foreach ($CPServers as $CPServer) {
if ($CPServer->pterodactyl_id) {
$CPIDArray[$CPServer->pterodactyl_id] = false;
}
}

foreach ($this->pterodactyl->getServers() as $server) { //go thru all ptero servers, if server exists, change value to true in array.
if (isset($CPIDArray[$server['attributes']['id']])) {
$CPIDArray[$server['attributes']['id']] = true;
// 3. Sync names and mark found servers
foreach ($this->pterodactyl->getServers() as $server) {
$pteroId = $server['attributes']['id'];
if (isset($CPIDArray[$pteroId])) {
$CPIDArray[$pteroId] = true;

if (isset($server['attributes']['name'])) { //failsafe
//Check if a server got renamed
$savedServer = Server::query()->where('pterodactyl_id', $server['attributes']['id'])->first();
if ($savedServer->name != $server['attributes']['name']) {
$savedServer->name = $server['attributes']['name'];
$savedServer->save();
if (isset($server['attributes']['name'])) {
$savedServer = $CPServers->where('pterodactyl_id', $pteroId)->first();
if ($savedServer && $savedServer->name != $server['attributes']['name']) {
$savedServer->update(['name' => $server['attributes']['name']]);
$renameCount++;
}
}
}
}
$filteredArray = array_filter($CPIDArray, function ($v, $k) {
return $v == false;
}, ARRAY_FILTER_USE_BOTH); //Array of servers, that dont exist on ptero (value == false)
$deleteCount = 0;
foreach ($filteredArray as $key => $CPID) { //delete servers that dont exist on ptero anymore
if (!$this->pterodactyl->getServerAttributes($key, true)) {

// 4. Delete servers that don't exist on Pterodactyl anymore
$orphanedServers = array_filter($CPIDArray, fn($found) => !$found);
foreach ($orphanedServers as $key => $found) {
try {
// getServerAttributes with deleteOn404=true will delete the server if it's missing
$this->pterodactyl->getServerAttributes($key, true);
$deleteCount++;
} catch (Exception $e) {
Log::error("Failed to check orphaned server {$key}: " . $e->getMessage());
}
}

return redirect()->back()->with('success', __('Servers synced successfully' . (($renameCount) ? (',\n' . __('renamed') . ' ' . $renameCount . ' ' . __('servers')) : '') . ((count($filteredArray)) ? (',\n' . __('deleted') . ' ' . $deleteCount . '/' . count($filteredArray) . ' ' . __('old servers')) : ''))) . '.';
$message = __('Servers synced successfully.');
if ($renameCount > 0) $message .= ' ' . __('Renamed') . ': ' . $renameCount . '.';
if ($recoveredCount > 0) $message .= ' ' . __('Recovered') . ': ' . $recoveredCount . '.';
if ($deleteCount > 0) $message .= ' ' . __('Deleted') . ': ' . $deleteCount . '.';

return redirect()->back()->with('success', $message);
}

/**
Expand Down Expand Up @@ -329,11 +379,16 @@ class="btn btn-sm '.$suspendColor.' text-white mr-1 suspend-btn"
</button>
</form>

<form class="d-inline" onsubmit="return submitResult();" method="post" action="' . route('admin.servers.destroy', $server->id) . '">
' . csrf_field() . '
' . method_field('DELETE') . '
<button data-content="' . __('Delete') . '" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
</form>
<button data-content="' . __('Delete') . '"
data-toggle="popover"
data-trigger="hover"
data-placement="top"
class="btn btn-sm btn-danger mr-1 delete-server-btn"
data-server-id="' . $server->id . '"
data-server-status="' . $server->status . '"
data-action="' . route('admin.servers.destroy', $server->id) . '">
<i class="fas fa-trash"></i>
</button>

';
})
Expand Down
44 changes: 42 additions & 2 deletions app/Http/Controllers/ServerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ public function store(Request $request): RedirectResponse
->with('error', __('Server creation failed'));
}

if ($server->status === Server::STATUS_PENDING_RECONCILIATION) {
return redirect()->route('servers.index')
->with('success', __('Server is being created, please wait.'));
}

return redirect()->route('servers.index')
->with('success', __('Server created'));
} catch (Exception $e) {
Expand Down Expand Up @@ -248,8 +253,14 @@ private function getServersWithInfo(): \Illuminate\Database\Eloquent\Collection
$servers = Auth::user()->servers;

foreach ($servers as $server) {
if (!$server->pterodactyl_id) {
continue;
}

$serverInfo = $this->pterodactyl->getServerAttributes($server->pterodactyl_id);
if (!$serverInfo) continue;
if (!$serverInfo) {
continue;
}

$this->updateServerInfo($server, $serverInfo);
}
Expand Down Expand Up @@ -315,6 +326,11 @@ public function destroy(Server $server): RedirectResponse
return back()->with('error', __('This is not your Server!'));
}

if (!$server->pterodactyl_id) {
Comment thread
MrWeez marked this conversation as resolved.
return redirect()->route('servers.index')
->with('error', __('Server is not ready yet. Please wait until it is created.'));
}

try {
$serverInfo = $this->pterodactyl->getServerAttributes($server->pterodactyl_id);

Expand Down Expand Up @@ -360,6 +376,11 @@ public function cancel(Server $server): RedirectResponse
return back()->with('error', __('This is not your Server!'));
}

if (!$server->pterodactyl_id) {
return redirect()->route('servers.index')
->with('error', __('Server is not ready yet. Please wait until it is created.'));
}

try {
$server->update(['canceled' => now()]);
return redirect()->route('servers.index')
Expand All @@ -370,12 +391,17 @@ public function cancel(Server $server): RedirectResponse
}
}

public function show(Server $server): \Illuminate\View\View
public function show(Server $server): \Illuminate\View\View|RedirectResponse
{
if ($server->user_id !== Auth::id()) {
return back()->with('error', __('This is not your Server!'));
}

if (!$server->pterodactyl_id) {
return redirect()->route('servers.index')
->with('error', __('Server is not ready yet. Please wait until it is created.'));
}

$serverAttributes = $this->pterodactyl->getServerAttributes($server->pterodactyl_id);
$upgradeOptions = $this->getUpgradeOptions($server, $serverAttributes);
return view('servers.settings')->with([
Expand Down Expand Up @@ -434,6 +460,11 @@ public function upgrade(Server $server, Request $request): RedirectResponse
->with('error', __('This is not your Server!'));
}

if (!$server->pterodactyl_id) {
return redirect()->route('servers.index')
->with('error', __('Server is not ready yet. Please wait until it is created.'));
}

if (!$request->has('product_upgrade')) {
return redirect()->route('servers.show', ['server' => $server->id])
->with('error', __('No product selected for upgrade'));
Expand Down Expand Up @@ -481,6 +512,11 @@ public function updateBillingPriority(Server $server, Request $request): Redirec
->with('error', __('This is not your Server!'));
}

if (!$server->pterodactyl_id) {
return redirect()->route('servers.index')
->with('error', __('Server is not ready yet. Please wait until it is created.'));
}

$server->update($data);

return redirect()->route('servers.show', ['server' => $server->id])
Expand All @@ -494,6 +530,10 @@ private function validateUpgrade(Server $server, Product $oldProduct, Product $n
return false;
}

if (!$server->pterodactyl_id) {
return false;
}

$serverInfo = $this->pterodactyl->getServerAttributes($server->pterodactyl_id);
if (!$serverInfo) {
return false;
Expand Down
67 changes: 24 additions & 43 deletions app/Jobs/ReconcileServerCreationJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,12 @@ public function handle(PterodactylClient $pterodactylClient, CreditService $cred
}

if ($response->status() === 404) {
// Atomic transition to FAILED to avoid double refunds in concurrent workers.
$updated = Server::where('id', $server->id)
->where('status', '!=', Server::STATUS_FAILED)
->update(['status' => Server::STATUS_FAILED]);

if ($updated === 1) {
$creditService->refund($server->user, $this->chargedPrice);
Log::info('ReconcileServerCreationJob: refunded credits on confirmed 404', [
'server_id' => $server->id,
'amount' => $this->chargedPrice,
]);
}
$server->delete();
Comment thread
MrWeez marked this conversation as resolved.
$creditService->refund($server->user, $this->chargedPrice);
Log::info('ReconcileServerCreationJob: deleted server and refunded credits on confirmed 404', [
'server_id' => $this->serverId,
'amount' => $this->chargedPrice,
]);

Comment thread
MrWeez marked this conversation as resolved.
return;
}
Expand Down Expand Up @@ -120,47 +114,34 @@ public function failed(\Throwable $exception): void
]);
dispatch(new PostServerCreationJob($server->id));

Log::critical('ReconcileServerCreationJob failed after retries but remote server found; marked active', [
Log::critical('ReconcileServerCreationJob exhausted retries but remote server finally found; marked active', [
'server_id' => $this->serverId,
'error' => $exception->getMessage(),
]);
return;
}
}

if ($response->status() === 404) {
$updated = Server::where('id', $server->id)
->where('status', '!=', Server::STATUS_FAILED)
->update(['status' => Server::STATUS_FAILED]);

if ($updated === 1) {
$creditService->refund($server->user, $this->chargedPrice);
Log::critical('ReconcileServerCreationJob failed after retries with remote 404; refunded credits', [
'server_id' => $this->serverId,
'amount' => $this->chargedPrice,
'error' => $exception->getMessage(),
]);
} else {
Log::info('ReconcileServerCreationJob failed after retries with remote 404; no refund needed because status already failed', [
'server_id' => $this->serverId,
]);
}

return;
}

$server->update(['status' => Server::STATUS_PENDING_RECONCILIATION]);
Log::critical('ReconcileServerCreationJob failed after maximum retries; remote state unknown, keeping pending_reconciliation', [
// If we are here, we either got a 404 or a persistent API error (500/timeout)
// after many retries. We satisfy the "automatic" requirement by cleaning up.
$server->delete();
$creditService->refund($server->user, $this->chargedPrice);

Log::critical('ReconcileServerCreationJob exhausted retries and could not confirm server; auto-deleted and refunded', [
'server_id' => $this->serverId,
'amount' => $this->chargedPrice,
'error' => $exception->getMessage(),
]);

} catch (\Exception $e) {
// If remote check also fails, preserve pending state and log.
$server->update(['status' => Server::STATUS_PENDING_RECONCILIATION]);
Log::critical('ReconcileServerCreationJob failed and remote check failed; keeping pending_reconciliation', [
'server_id' => $this->serverId,
'exception' => $e->getMessage(),
]);
// Even if the final check fails, we delete and refund to avoid "stuck" servers.
if ($server->exists) {
$server->delete();
$creditService->refund($server->user, $this->chargedPrice);
Log::critical('ReconcileServerCreationJob failed critically (even final check); forced delete and refund', [
'server_id' => $this->serverId,
'exception' => $e->getMessage()
]);
}
}
}
}
Loading
Loading