diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 90d4c10..31bd5cd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -46,13 +46,11 @@ jobs: key: '${{ runner.OS }}-build-${{ hashFiles(''**/composer.lock'') }}' - name: Copy .env run: php -r "file_exists('.env') || copy('.env.example', '.env');" # If .env exist, we use that, if otherwise, copy .env.example to .env and use that instead + - name: Create DB File + run: touch database/database.sqlite - name: Install Dependencies if: steps.vendor-cache.outputs.cache-hit != 'true' run: composer install -q --no-ansi --no-interaction --no-dev --no-progress --prefer-dist - - name: Generate key - run: php artisan key:generate - - name: Clear Config - run: php artisan config:clear - name: Create an Archive For Release uses: montudor/action-zip@v0.1.0 with: diff --git a/README.md b/README.md index 179ba7a..6122778 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,47 @@ -# Palworld Admin Toll +# Palworld Admin Tool ## Description - An Admin tool receives information via rcon and displays them. -## Prerequisites - -The following packages need to be installed: - -1. php (8.1 or higher) with the following extensions +## Overview +1. [Installation Variations](#install-possibilities) +2. [Installation via Release (1 click install)](#installation-via-release) +3. [Full Installation](#full-installation) + 1. [Prerequisites full Installation](#prerequisites-developerfull-installation) + 2. [Install Steps](#installation-steps) +3. [Updating](#updating) + +## Installation Variations +You can install this tool in the following ways +- Install via docker-compose + - Head over to https://github.com/Insax/palworld-admin-tool-docker and read the installation instructions +- Install using the latest [release](https://github.com/Insax/palworld-admin-tool/releases/latest) for Windows Users. + - Go to [Installation via Release](#installation-via-release) and follow the steps (1 click install) +- Install cloning the repository (Advanced/Developer Installation) + - Go to [Developer Setup](#developer-installation) + + +## Installation via Release +> :warning: **This way of installing does not support updating your installation and has limitations.** + +#### This way of installing provides a fully working instance for testing purposes, its not meant to be used in real production. + +#### If you like it go for the docker version or the full install. + +Steps +1. Download the release as zip and extract it somewhere +2. Run the script install-start.ps1 using powershell. +3. Visit http://localhost + +> :warning: **Once again, this is more of a Test installation than anything else.** + +## Full Installation +This will provide you with everything to update or develop the app yourself. + +### Prerequisites Developer/Full Installation +This application has some requirements that must be fullfilled in order to for everything to work properly. + +1. PHP 8.1 with the following extensions enabled:** - ctype - curl - dom @@ -22,13 +55,14 @@ The following packages need to be installed: - session - tokenizer - xml -15. composer (https://getcomposer.org/) -16. npm (20 or higher) https://nodejs.org/en/download -17. Supervisord or an equivalent or http://supervisord.org/ -18. Nginx or an equivalent https://nginx.org/en/download.html -19. Any Mysql or Postgres Database that supports column type `enum` + - sqlite +1. composer (https://getcomposer.org/) +2. npm (20 or higher) https://nodejs.org/en/download +3. Supervisord or an equivalent http://supervisord.org/ +4. Nginx or an equivalent https://nginx.org/en/download.html +5. [Optional] Any Mysql or Postgres Database, alternatively sqlite can be used. -## Installation +### Installation Steps 1. Clone the repository @@ -50,24 +84,35 @@ The following packages need to be installed: npm run build ``` 4. Copy .env.example to .env and adjust the DB_HOST, DB_PORT, DB_USER, DB_DATABASE, DB_PASSWORD so it matches your setup + + 1. If you would like to use SQLITE set `DB_CONNECTION` to `sqlite` and delete the `DB_DATABASE` line. ```bash cp .env.example .env ``` -4. Generate an application key + +5. Create the database tables in your already created database. + + ```bash + php artisan migrate --force + ``` + +6. Generate an application key ```bash php artisan key:generate ``` -4. Create a job in supervisor or an equivalent tool that auto restarts and runs + +7. Create a job in supervisor or an equivalent tool that auto restarts and runs ```bash - php artisan short-schedule:run --lifetime=60 - ``` -5. Adjust the connections in config/rcon.php so they match you servers. Do not edit the default entry, it will not show up the application. + php artisan short-schedule:run + ``` +8. Configure your webserver, the content root is in `public` + -6. Configure your webserver, the content root is in `public` -7. Visit the website, the installer should pop up. +## Updating +Rerun steps 2 - 5 ## Running Tests diff --git a/app/Console/Commands/SyncPlayersCommand.php b/app/Console/Commands/SyncPlayersCommand.php index c20472f..93d982f 100644 --- a/app/Console/Commands/SyncPlayersCommand.php +++ b/app/Console/Commands/SyncPlayersCommand.php @@ -2,26 +2,25 @@ namespace App\Console\Commands; -use App\Models\JoinAndLeave; +use App\Gameserver\Communication\Responses\Response; +use App\Gameserver\Communication\Responses\ShowPlayersResponse; +use App\Models\JoinLeaveLog; use App\Models\Player; use App\Models\Server; use App\Models\ServerWhitelist; use Illuminate\Console\Command; -use RCON; +use Rcon; class SyncPlayersCommand extends Command { + /** * The name and signature of the console command. - * - * @var string */ protected $signature = 'pal:sync'; /** * The console command description. - * - * @var string */ protected $description = 'Synchronizes Players from a PalWorldServer'; @@ -30,58 +29,109 @@ class SyncPlayersCommand extends Command */ public function handle() { - foreach (Server::whereActive(true)->get() as $server) - { - $onlinePlayers = array(); - $result = RCON::getPlayers($server->rcon); - foreach ($result as $player) { - if($player['player_id'] == 00000000) - continue; - - $onlinePlayers[] = $player['player_id']; - - $player['online'] = true; - $player['server_id'] = $server->id; - $players = Player::where(['player_id' => $player['player_id'], 'server_id' => $player['server_id']])->first(); - - if(is_null($players)) - { - $newPlayer = Player::create($player); - JoinAndLeave::create(['player_id' => $newPlayer->id, 'action' => JoinAndLeave::$PLAYER_JOINED]); - } - else - { - if($players->online == false) - { - JoinAndLeave::create(['player_id' => $players->id, 'action' => JoinAndLeave::$PLAYER_JOINED]); - } - $players->update($player); - } - } - $offlinePlayers = Player::where('server_id', $server->id)->whereNotIn('player_id', $onlinePlayers)->whereOnline(true)->get(); + $servers = Server::whereActive(true)->with(['rconData', 'serverWhitelists', 'players'])->get(); - foreach ($offlinePlayers as $offlinePlayer) - { - $offlinePlayer->update(['online' => false]); - JoinAndLeave::create(['player_id' => $offlinePlayer->id, 'action' => JoinAndLeave::$PLAYER_LEFT]); + foreach ($servers as $server) { + $response = Rcon::info($server); + if($response->getError() != 0) { + $this->handleUnreachableServer($server); + continue; } - if($server->uses_whitelist) - { - $whitelist = ServerWhitelist::where('server_id', $server->id)->get(); - $whitelistPlayers = array(); - foreach ($whitelist as $whitelistItem) { - $whitelistPlayers[] = $whitelistItem->player_id; - } - - $notWhitelistedPlayers = Player::where('server_id', $server->id)->whereNotIn('player_id', $whitelistPlayers)->get(); - - foreach ($notWhitelistedPlayers as $notWhitelistedPlayer) { - RCON::kickPlayer($server->rcon, $notWhitelistedPlayer->player_id); - $notWhitelistedPlayer->update(['online' => false]); - JoinAndLeave::create(['player_id' => $notWhitelistedPlayer->id, 'action' => JoinAndLeave::$PLAYER_KICKED_WHITELIST]); - } + if(!$server->online) + $server->update(['online' => true]); + + $this->syncServerPlayers($server); + } + } + + private function handleUnreachableServer(Server $server) + { + $server->shutting_down = false; + $server->online = false; + $this->handleOfflinePlayers($server, []); + } + + private function syncServerPlayers(Server $server) + { + $onlinePlayers = $this->getOnlinePlayers($server); + + + $this->handleOfflinePlayers($server, $onlinePlayers); + + if ($server->uses_whitelist) { + $this->handleNotWhitelistedPlayers($server); + } + } + + private function getOnlinePlayers(Server $server): array + { + $onlinePlayersIDs = []; + $result = Rcon::showPlayers($server); + + foreach ($result->getResult() as $player) { + if ($player['player_id'] == '00000000') continue; + + $onlinePlayersIDs[] = $player['player_id']; + $this->updatePlayerStatus($player, $server); + } + + return $onlinePlayersIDs; + } + + private function updatePlayerStatus(array $player, Server $server): void + { + $playerData = [ + 'online' => true, + 'server_id' => $server->id, + 'player_id' => $player['player_id'], + 'steam_id' => $player['steam_id'], + 'name' => $player['name'] + ]; + + $playerModel = Player::wherePlayerId($playerData['player_id'])->whereServerId($player['server_id'])->first(); + + if ($playerModel->exists){ + if (!$playerModel->online) { + $this->logPlayerAction($playerModel, JoinLeaveLog::$PLAYER_JOINED); } + $playerModel->update($playerData); + } else { + $playerModel = Player::create($playerData); + $this->logPlayerAction($playerModel, JoinLeaveLog::$PLAYER_JOINED); + } + } + + private function handleOfflinePlayers(Server $server, array $onlinePlayers): void + { + $offlinePlayers = Player::where('server_id', $server->id) + ->whereNotIn('player_id', $onlinePlayers) + ->whereOnline(true) + ->get(); + + foreach ($offlinePlayers as $offlinePlayer) { + $offlinePlayer->update(['online' => false]); + $this->logPlayerAction($offlinePlayer, JoinLeaveLog::$PLAYER_LEFT); + } + } + + private function handleNotWhitelistedPlayers(Server $server): void + { + $whitelistIDs = $server->serverWhitelists->pluck('player_id')->toArray(); + + $notWhitelistedPlayers = Player::whereServerId($server->id) + ->whereNotIn('player_id', $whitelistIDs) + ->get(); + + foreach ($notWhitelistedPlayers as $player) { + Rcon::kickPlayer($server, $player->player_id); + $player->update(['online' => false]); + $this->logPlayerAction($player, JoinLeaveLog::$PLAYER_KICKED_WHITELIST); } } + + private function logPlayerAction(Player $player, string $action): void + { + JoinLeaveLog::create(['player_id' => $player->id, 'action' => $action]); + } } diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 7d455e9..8443ab8 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -18,11 +18,14 @@ protected function schedule(Schedule $schedule): void /** * Short Schedule to get the job time down to 5 seconds. + * + * @param \Spatie\ShortSchedule\ShortSchedule $shortSchedule + * @return void */ - protected function shortSchedule(\Spatie\ShortSchedule\ShortSchedule $shortSchedule) + protected function shortSchedule(\Spatie\ShortSchedule\ShortSchedule $shortSchedule): void { // this command will run every second - $shortSchedule->command('pal:sync')->everySeconds(5); + $shortSchedule->command('pal:sync')->everySeconds(5)->withoutOverlapping(); } /** diff --git a/app/PalWorld/RCON/Facades/Facade.php b/app/Facades/Rcon.php similarity index 69% rename from app/PalWorld/RCON/Facades/Facade.php rename to app/Facades/Rcon.php index 212896a..c51ac66 100644 --- a/app/PalWorld/RCON/Facades/Facade.php +++ b/app/Facades/Rcon.php @@ -1,10 +1,10 @@ rconData->host, $server->rconData->port, \Crypt::decrypt($server->rconData->password), $server->rconData->timeout); + $response = $rcon->command('info'); + return new InfoResponse($response); + } + + public function showPlayers(Server $server) : Response + { + $rcon = new PalworldRcon($server->rconData->host, $server->rconData->port, \Crypt::decrypt($server->rconData->password), $server->rconData->timeout); + $response = $rcon->command('showPlayers'); + return new ShowPlayersResponse($response); + } + + public function kickPlayer(Server $server, string|int $playerId) : Response + { + $rcon = new PalworldRcon($server->rconData->host, $server->rconData->port, \Crypt::decrypt($server->rconData->password), $server->rconData->timeout); + $response = $rcon->command("kick $playerId"); + return new KickPlayerResponse($response); + } + + public function banPlayer(Server $server, string|int $playerId) : Response + { + $rcon = new PalworldRcon($server->rconData->host, $server->rconData->port, \Crypt::decrypt($server->rconData->password), $server->rconData->timeout); + $response = $rcon->command("ban $playerId"); + return new BanPlayerResponse($response); + } + + public function broadcast(Server $server, string $message) : Response + { + $message = str_replace($message, ' ', '\x1f'); + $rcon = new PalworldRcon($server->rconData->host, $server->rconData->port, \Crypt::decrypt($server->rconData->password), $server->rconData->timeout); + $response = $rcon->command("broadcast $message"); + return new BroadcastResponse($response); + } + + public function save(Server $server) + { + $rcon = new PalworldRcon($server->rconData->host, $server->rconData->port, \Crypt::decrypt($server->rconData->password), $server->rconData->timeout); + $response = $rcon->command("save"); + return new SaveResponse($response); + } +} diff --git a/app/Gameserver/Communication/Responses/BanPlayerResponse.php b/app/Gameserver/Communication/Responses/BanPlayerResponse.php new file mode 100644 index 0000000..6beb0bb --- /dev/null +++ b/app/Gameserver/Communication/Responses/BanPlayerResponse.php @@ -0,0 +1,32 @@ + substr($this->getHeader(), 8)]; + } + + protected function isExpectedHeaderText(string $header): bool + { + return !strncmp('Banned: ', $header, 8); + } +} diff --git a/app/Gameserver/Communication/Responses/BroadcastResponse.php b/app/Gameserver/Communication/Responses/BroadcastResponse.php new file mode 100644 index 0000000..0da1bfa --- /dev/null +++ b/app/Gameserver/Communication/Responses/BroadcastResponse.php @@ -0,0 +1,32 @@ + substr($this->getHeader(),14, null)]; + } + + protected function isExpectedHeaderText(string $header): bool + { + return !strncmp('Broadcasted: ', $header, 13); + } +} diff --git a/app/Gameserver/Communication/Responses/InfoResponse.php b/app/Gameserver/Communication/Responses/InfoResponse.php new file mode 100644 index 0000000..ea1dbc3 --- /dev/null +++ b/app/Gameserver/Communication/Responses/InfoResponse.php @@ -0,0 +1,34 @@ +getHeader(), $matches); + return ['version' => $matches[1], 'serverName' => $matches[2]]; + } + + protected function isExpectedHeaderText(string $header) : bool + { + return !strncmp('Welcome to Pal Server', $header, 21); + } +} diff --git a/app/Gameserver/Communication/Responses/KickPlayerResponse.php b/app/Gameserver/Communication/Responses/KickPlayerResponse.php new file mode 100644 index 0000000..d111bb7 --- /dev/null +++ b/app/Gameserver/Communication/Responses/KickPlayerResponse.php @@ -0,0 +1,32 @@ + substr($this->getHeader(), 8)]; + } + + protected function isExpectedHeaderText(string $header): bool + { + return !strncmp('Kicked: ', $header, 8); + } +} diff --git a/app/Gameserver/Communication/Responses/Response.php b/app/Gameserver/Communication/Responses/Response.php new file mode 100644 index 0000000..52c7318 --- /dev/null +++ b/app/Gameserver/Communication/Responses/Response.php @@ -0,0 +1,87 @@ +extractHeader(); + if($this->isErrorResponse) + return; + $this->result = $this->extractBody($this->lines); + } + + public function getError() + { + return $this->error; + } + + public function getResult() : array + { + return $this->result; + } + + public function getHeader() : string + { + return $this->header; + } + + protected abstract function extractBody(array $lines) : array; + + private function extractHeader() + { + $this->lines = explode("\n", $this->responseText); + $this->header = array_shift($this->lines); + $this->checkHeader(); + } + + protected abstract function isExpectedHeaderText(string $header) : bool; + + private function checkHeader() + { + if($this->isExpectedHeaderText($this->header)) + return; + + $this->isErrorResponse = true; + + if($this->header == 'Authorization rejected') + { + $this->error = self::ERROR_AUTH; + \Log::alert($this->header); + } + elseif (strncmp('Could not connect to RCON', $this->header, 25)) + { + $this->error = self::ERROR_CONNECT; + \Log::critical($this->header); + } + else + { + $this->error = self::ERROR_CMD; + \Log::info('Command Failed'); + } + } +} diff --git a/app/Gameserver/Communication/Responses/SaveResponse.php b/app/Gameserver/Communication/Responses/SaveResponse.php new file mode 100644 index 0000000..c106918 --- /dev/null +++ b/app/Gameserver/Communication/Responses/SaveResponse.php @@ -0,0 +1,32 @@ + $playerData[0], + 'player_id' => $playerData[1], + 'steam_id' => $playerData[2] + ]; + } + return $body; + } + + protected function isExpectedHeaderText(string $header): bool + { + return $header == 'name,playeruid,steamid'; + } +} diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php deleted file mode 100644 index 746087e..0000000 --- a/app/Http/Controllers/DashboardController.php +++ /dev/null @@ -1,10 +0,0 @@ - 'required', + 'port' => 'required', + 'password' => 'required', + 'testSuccessful' => [ + 'required', + Rule::in(true) + ] + ]; + } + + public function mount() + { + + } + + public function testConnection() + { + $rcon = new PalworldRcon($this->ip, $this->port, $this->password, 5); + $result = new InfoResponse($rcon->command('info')); + if ($result->getError() == 0) { + $this->testSuccessful = true; + Toaster::success("RCON Connection to ".$result->getResult()['serverName']." successful"); + } else + Toaster::error($result->getHeader()); + + $this->rconResponse = $result->getHeader(); + $this->quickServerName = $result->getResult()['serverName']; + } + + public function addRcon() + { + $this->testConnection(); + $this->validate(); + + $rcon = RconData::create(['host' => $this->ip, 'port' => $this->port, 'password' => \Crypt::encrypt($this->password)]); + + if($this->quickServer) { + $server = Server::create(['name' => $this->quickServerName, 'rcon_data_id' => $rcon->id]); + return redirect()->route('server-dashboard', ['id' => $server->id]); + } + + $this->reset(['ip', 'port', 'password', 'testSuccessful', 'quickServer', 'quickServerName', 'rconResponse']); + Toaster::success('Rcon Connection Created'); + } + + public function render() + { + return view('livewire.add-rcon')->layout('layouts.app'); + } +} diff --git a/app/Livewire/AddServer.php b/app/Livewire/AddServer.php index 93548e6..e237166 100644 --- a/app/Livewire/AddServer.php +++ b/app/Livewire/AddServer.php @@ -2,9 +2,11 @@ namespace App\Livewire; +use App\Models\RconData; use App\Models\Server; use Illuminate\Validation\Rule; use Livewire\Component; +use Masmerise\Toaster\Toaster; use Spatie\Permission\Models\Permission; class AddServer extends Component @@ -29,11 +31,9 @@ public function rules() public function mount() { - foreach (config('rcon.connections') as $conn => $values) + foreach (RconData::get() as $connection) { - if($conn == 'default') - continue; - $this->availableRCON[] = $conn; + $this->availableRCON[] = ['value' => $connection->id, 'text' => $connection->host.':'.$connection->port]; } } @@ -49,6 +49,7 @@ public function addServer() Permission::create(['name' => 'Restart Server ['.$server->id.']']); Permission::create(['name' => 'Edit Whitelist Server ['.$server->id.']']); + Toaster::success('Server created'); return redirect()->route('server-dashboard', ['id' => $server->id]); } diff --git a/app/Livewire/EditRcon.php b/app/Livewire/EditRcon.php new file mode 100644 index 0000000..5b9b967 --- /dev/null +++ b/app/Livewire/EditRcon.php @@ -0,0 +1,69 @@ + 'required', + 'ip' => 'required', + 'password' => 'required', + 'testSuccessful' => [ + 'required', + Rule::in(true) + ] + ]; + } + + public function testConnection() + { + $rcon = new PalworldRcon($this->ip, $this->port, $this->password, 5); + $result = new InfoResponse($rcon->command('info')); + if ($result->getError() == 0) { + $this->testSuccessful = true; + Toaster::success("RCON Connection to ".$result->getResult()['serverName']." successful"); + } else + Toaster::error($result->getHeader()); + + $this->rconResponse = $result->getHeader(); + $this->quickServerName = $result->getResult()['serverName']; + } + + public function mount($id) + { + $this->rconData = RconData::find($id); + $this->port = $this->rconData->port; + $this->ip = $this->rconData->host; + } + + public function editRcon() + { + $this->testConnection(); + $this->validate(); + + $this->rconData->update(['host' => $this->ip, 'port' => $this->port, 'password' => $this->password]); + Toaster::success('RCON successfully edited'); + return redirect()->route('rcon-overview'); + } + + public function render() + { + return view('livewire.edit-rcon')->layout('layouts.app'); + } +} diff --git a/app/Livewire/EditServer.php b/app/Livewire/EditServer.php index 208111b..2903075 100644 --- a/app/Livewire/EditServer.php +++ b/app/Livewire/EditServer.php @@ -2,6 +2,7 @@ namespace App\Livewire; +use App\Models\RconData; use App\Models\Server; use Illuminate\Validation\Rule; use Livewire\Component; @@ -13,20 +14,18 @@ class EditServer extends Component public array $availableRCON; public string $name; - public string $rcon; + public int $rcon; public bool $uses_whitelist = false; public function mount($id) { - foreach (config('rcon.connections') as $conn => $values) + foreach (RconData::get() as $connection) { - if($conn == 'default') - continue; - $this->availableRCON[] = $conn; + $this->availableRCON[] = ['value' => $connection->id, 'text' => $connection->host.':'.$connection->port]; } $this->server = Server::find($id); $this->name = $this->server->name; - $this->rcon = $this->server->rcon; + $this->rcon = $this->server->rcon_data_id; $this->uses_whitelist = $this->server->uses_whitelist; } diff --git a/app/Livewire/RconOverview.php b/app/Livewire/RconOverview.php new file mode 100644 index 0000000..3e658cf --- /dev/null +++ b/app/Livewire/RconOverview.php @@ -0,0 +1,45 @@ +connections = RconData::get(); + } + + public function deleteRcon(RconData $rconData) + { + $serverExists = false; + foreach ($rconData->servers as $server) + { + Toaster::error('Can\'t delete RCON, Server '. $server->name .' is still connected to it.'); + $serverExists = true; + } + + if(!$serverExists) { + $rconData->delete(); + Toaster::success('RCON successfully deleted.'); + } + } + + public function toggleActiveServer(Server $server) + { + $server->active = !$server->active; + $server->save(); + return redirect()->route('server-overview'); + } + + public function render() + { + return view('livewire.rcon-overview')->layout('layouts.app'); + } +} diff --git a/app/Livewire/ServerDashboard.php b/app/Livewire/ServerDashboard.php index d32aa38..55df0b7 100644 --- a/app/Livewire/ServerDashboard.php +++ b/app/Livewire/ServerDashboard.php @@ -2,11 +2,12 @@ namespace App\Livewire; -use App\Models\JoinAndLeave; +use App\Models\JoinLeaveLog; use App\Models\Player; use App\Models\Server; -use RCON; +use Masmerise\Toaster\Toaster; use Livewire\Component; +use Rcon; class ServerDashboard extends Component { @@ -24,18 +25,21 @@ public function mount($id) public function kickPlayer($player) { - $rcon = Server::find($this->id)->rcon; - RCON::kickPlayer($rcon, $player['player_id']); - JoinAndLeave::create(['player_id' => $player['id'], 'action' => JoinAndLeave::$PLAYER_KICKED_USER]); - return redirect()->route('server-dashboard', ['id' => $this->id]); + $server = Server::find($this->id); + Rcon::kickPlayer($server, $player['player_id']); + JoinLeaveLog::create(['player_id' => $player['id'], 'action' => JoinLeaveLog::$PLAYER_KICKED_USER]); + Player::whereId($player['id'])->update(['online' => false]); + Rcon::broadcast($server, 'Kicked_Player: '.$player['name']); + Toaster::success('Player kicked'); } public function banPlayer($player) { - $rcon = Server::find($this->id)->rcon; - RCON::banPlayer($rcon, $player['player_id']); - JoinAndLeave::create(['player_id' => $player['id'], 'action' => JoinAndLeave::$PLAYER_BAN_USR]); - return redirect()->route('server-dashboard', ['id' => $this->id]); + $server = Server::find($this->id); + Rcon::banPlayer($server, $player['player_id']); + JoinLeaveLog::create(['player_id' => $player['id'], 'action' => JoinLeaveLog::$PLAYER_BAN_USR]); + Player::whereId($player['id'])->update(['online' => false]); + Toaster::success('Player banned'); } public function buildPlayerList() @@ -46,13 +50,14 @@ public function buildPlayerList() public function buildJoinLeaveLog() { - $this->joinLeaveLog = JoinAndLeave::whereRelation('player', 'server_id', $this->id)->orderBy('created_at', 'desc')->with('player')->limit(200)->get(); + $this->joinLeaveLog = JoinLeaveLog::whereRelation('player', 'server_id', $this->id)->orderBy('created_at', 'desc')->with('player')->limit(200)->get(); } public function shutdownServer() { - $rcon = Server::find($this->id)->rcon; - \RCON::shutdownServer($rcon); + $server = Server::find($this->id); + Rcon::shutdownServer($server); + Toaster::success('Shutdown Initialized'); } public function render() diff --git a/app/Livewire/ServerOverview.php b/app/Livewire/ServerOverview.php index a76c28c..70713ba 100644 --- a/app/Livewire/ServerOverview.php +++ b/app/Livewire/ServerOverview.php @@ -4,6 +4,7 @@ use App\Models\Server; use Livewire\Component; +use Masmerise\Toaster\Toaster; class ServerOverview extends Component { @@ -11,7 +12,15 @@ class ServerOverview extends Component public function mount() { - $this->servers = Server::get(); + $this->servers = Server::with('rconData')->get(); + } + + public function deleteServer(Server $server) + { + $server->serverWhitelists()->delete(); + $server->players()->delete(); + $server->delete(); + Toaster::success('Server has been successfully deleted'); } public function toggleActiveServer(Server $server) diff --git a/app/Models/JoinAndLeave.php b/app/Models/JoinLeaveLog.php similarity index 51% rename from app/Models/JoinAndLeave.php rename to app/Models/JoinLeaveLog.php index 0db266f..d953a2a 100644 --- a/app/Models/JoinAndLeave.php +++ b/app/Models/JoinLeaveLog.php @@ -10,7 +10,7 @@ use Illuminate\Database\Eloquent\Model; /** - * Class JoinAndLeave + * Class JoinLeaveLog * * @property int $id * @property int $player_id @@ -19,19 +19,19 @@ * @property Carbon|null $updated_at * @property Player $player * @package App\Models - * @method static \Illuminate\Database\Eloquent\Builder|JoinAndLeave newModelQuery() - * @method static \Illuminate\Database\Eloquent\Builder|JoinAndLeave newQuery() - * @method static \Illuminate\Database\Eloquent\Builder|JoinAndLeave query() - * @method static \Illuminate\Database\Eloquent\Builder|JoinAndLeave whereAction($value) - * @method static \Illuminate\Database\Eloquent\Builder|JoinAndLeave whereCreatedAt($value) - * @method static \Illuminate\Database\Eloquent\Builder|JoinAndLeave whereId($value) - * @method static \Illuminate\Database\Eloquent\Builder|JoinAndLeave wherePlayerId($value) - * @method static \Illuminate\Database\Eloquent\Builder|JoinAndLeave whereUpdatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|JoinLeaveLog newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|JoinLeaveLog newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|JoinLeaveLog query() + * @method static \Illuminate\Database\Eloquent\Builder|JoinLeaveLog whereAction($value) + * @method static \Illuminate\Database\Eloquent\Builder|JoinLeaveLog whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|JoinLeaveLog whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|JoinLeaveLog wherePlayerId($value) + * @method static \Illuminate\Database\Eloquent\Builder|JoinLeaveLog whereUpdatedAt($value) * @mixin \Eloquent */ -class JoinAndLeave extends Model +class JoinLeaveLog extends Model { - protected $table = 'join_and_leave'; + protected $table = 'join_leave_log'; protected $casts = [ 'player_id' => 'int' diff --git a/app/Models/Player.php b/app/Models/Player.php index 88692d6..f913094 100644 --- a/app/Models/Player.php +++ b/app/Models/Player.php @@ -22,9 +22,9 @@ * @property Carbon|null $updated_at * @property int|null $server_id * @property Server|null $server - * @property Collection|JoinAndLeave[] $join_and_leaves + * @property Collection|JoinLeaveLog[] $joinLeaveLogs * @package App\Models - * @property-read int|null $join_and_leaves_count + * @property-read int|null $joinLeaveLogs_count * @method static \Illuminate\Database\Eloquent\Builder|Player newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|Player newQuery() * @method static \Illuminate\Database\Eloquent\Builder|Player query() @@ -60,8 +60,8 @@ public function server() return $this->belongsTo(Server::class); } - public function join_and_leaves() + public function joinLeaveLogs() { - return $this->hasMany(JoinAndLeave::class); + return $this->hasMany(JoinLeaveLog::class); } } diff --git a/app/Models/RconData.php b/app/Models/RconData.php new file mode 100644 index 0000000..c9e9423 --- /dev/null +++ b/app/Models/RconData.php @@ -0,0 +1,62 @@ + 'int', + 'timeout' => 'int' + ]; + + protected $hidden = [ + 'password' + ]; + + protected $fillable = [ + 'host', + 'port', + 'password', + 'timeout' + ]; + + public function servers() + { + return $this->hasMany(Server::class, 'rcon_data_id'); + } +} diff --git a/app/Models/Server.php b/app/Models/Server.php index 9aba460..e977bec 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -15,18 +15,19 @@ * * @property int $id * @property string $name - * @property string $rcon * @property bool $online * @property bool $active * @property Carbon|null $created_at * @property Carbon|null $updated_at * @property bool $shutting_down * @property bool $uses_whitelist + * @property int $rcon_data_id + * @property RconData $rconData * @property Collection|Player[] $players - * @property Collection|ServerWhitelist[] $server_whitelists + * @property Collection|ServerWhitelist[] $serverWhitelists * @package App\Models * @property-read int|null $players_count - * @property-read int|null $server_whitelists_count + * @property-read int|null $serverWhitelists_count * @method static \Illuminate\Database\Eloquent\Builder|Server newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|Server newQuery() * @method static \Illuminate\Database\Eloquent\Builder|Server query() @@ -35,7 +36,7 @@ * @method static \Illuminate\Database\Eloquent\Builder|Server whereId($value) * @method static \Illuminate\Database\Eloquent\Builder|Server whereName($value) * @method static \Illuminate\Database\Eloquent\Builder|Server whereOnline($value) - * @method static \Illuminate\Database\Eloquent\Builder|Server whereRcon($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereRconDataId($value) * @method static \Illuminate\Database\Eloquent\Builder|Server whereShuttingDown($value) * @method static \Illuminate\Database\Eloquent\Builder|Server whereUpdatedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|Server whereUsesWhitelist($value) @@ -49,24 +50,30 @@ class Server extends Model 'online' => 'bool', 'active' => 'bool', 'shutting_down' => 'bool', - 'uses_whitelist' => 'bool' + 'uses_whitelist' => 'bool', + 'rcon_data_id' => 'int' ]; protected $fillable = [ 'name', - 'rcon', 'online', 'active', 'shutting_down', - 'uses_whitelist' + 'uses_whitelist', + 'rcon_data_id' ]; + public function rconData() + { + return $this->belongsTo(RconData::class, 'rcon_data_id'); + } + public function players() { return $this->hasMany(Player::class); } - public function server_whitelists() + public function serverWhitelists() { return $this->hasMany(ServerWhitelist::class); } diff --git a/app/PalWorld/RCON/Connection.php b/app/PalWorld/RCON/Connection.php deleted file mode 100644 index 605818f..0000000 --- a/app/PalWorld/RCON/Connection.php +++ /dev/null @@ -1,209 +0,0 @@ -host = $host; - $this->port = $port; - $this->timeout = $timeout; - $this->rcon = $rcon; - } - - /** - * Connect to RCON server. - * - * @throws Exception - * @return void - */ - public function connect() - { - try { - $this->socket = fsockopen($this->host, $this->port, $errno, - $errstr, $this->timeout); - } catch (\Exception $ex) {} - - if (! $this->isConnected()) { - $servers = Server::where('rcon', $this->rcon)->get(); - foreach ($servers as $server) { - $server->update(['shutting_down' => false]); - $server->update(['online' => false]); - } - throw new Exception("Socket error: $errstr ($errno)"); - } - } - - /** - * Disconnect from RCON server if connection is established. - * - * @return void - */ - public function disconnect() - { - if ($this->isConnected()) { - fclose($this->socket); - } - } - - /** - * Check if connection to RCON server is established. - * - * @return bool - */ - public function isConnected() - { - return is_resource($this->socket); - } - - /** - * Check if connection is established before - * sending data. - * - * @return void - * @throws Exception - */ - protected function checkConnection() - { - if (! $this->isConnected()) { - $this->connect(); - } - } - - /** - * Authorize connection with given password. - * - * @param string $password - * @return bool - */ - public function authorize($password) - { - $this->checkConnection(); - - $this->authorized = false; - - $response = $this->send( - Packet::ID_AUTHORIZE, Packet::TYPE_SERVERDATA_AUTH, $password - ); - - if ($response->getId() == Packet::ID_AUTHORIZE && - $response->getType() == Packet::TYPE_SERVERDATA_AUTH_RESPONSE) { - $this->authorized = true; - } - - return $this->isAuthorized(); - } - - /** - * Check if connection is authorized. - * - * @return bool - */ - public function isAuthorized() - { - return $this->authorized; - } - - /** - * Execute given command on RCON server. - * - * @param string $command - * @return string - * @throws Exception - */ - public function command($command) - { - $this->checkConnection(); - - $response = $this->send( - Packet::ID_COMMAND, Packet::TYPE_SERVERDATA_EXECCOMMAND, $command - ); - - if ($response->getId() == Packet::TYPE_SERVERDATA_RESPONSE_VALUE && - $response->getType() == Packet::TYPE_SERVERDATA_RESPONSE_VALUE) { - return $response->getBody(); - } - - throw new Exception('Received invalid response'); - } - - /** - * Send packet do RCON server and receive response. - * - * @param int $id - * @param int $type - * @param string $body - * @return Packet - */ - public function send($id, $type, $body) - { - $this->sendPacket($id, $type, $body); - - return $this->receivePacket(); - } - - /** - * Generate packet binary structure used by RCON server - * and send it through socket. - * - * @param int $id - * @param int $type - * @param string $body - * @return void - */ - public function sendPacket($id, $type, $body) - { - $this->checkConnection(); - - $packet = Packet::fromFields( - compact('id', 'type', 'body') - ); - - fwrite($this->socket, $packet->getBytes()); - } - - /** - * Receive packet from RCON server and decode its - * binary structure to data object. - * - * @return Packet - */ - public function receivePacket() - { - $this->checkConnection(); - - $bytes = fread($this->socket, 4); - $size = unpack('V1size', $bytes); - - $bytes .= fread($this->socket, $size['size']); - - return Packet::fromBytes($bytes); - } -} diff --git a/app/PalWorld/RCON/ConnectionInterface.php b/app/PalWorld/RCON/ConnectionInterface.php deleted file mode 100644 index 9cbdf06..0000000 --- a/app/PalWorld/RCON/ConnectionInterface.php +++ /dev/null @@ -1,47 +0,0 @@ -connections = []; - $this->defaultConnectionName = config('rcon.default', 'default'); - } - - /** - * Make connection with given name in config. - * - * @param string $name - * @return Connection - */ - protected function makeConnection($name) - { - $config = $this->getConnectionConfig($name); - - if (is_null($config)) { - throw new Exception("Connection $name does not exists in config"); - } - - $connection = new Connection($config['host'], $config['port'], $config['timeout'], $name); - - if (array_key_exists('password', $config) && isset($config['password'])) { - $connection->authorize($config['password']); - } - - return $this->connections[$name] = $connection; - } - - /** - * Return connection config by its name. - * - * @param string $name - * @return array - */ - public function getConnectionConfig($name) - { - return config('rcon.connections.' . $name); - } - - /** - * Return given connection by its name. If connection is not - * established creates new connection. - * - * @param string $name - * @return Connection - */ - public function connection($name) - { - if (! array_key_exists($name, $this->connections)) { - $this->makeConnection($name); - } - - return $this->connections[$name]; - } - - /** - * Return default connection set in config. - * - * @return Connection - */ - public function defaultConnection() - { - return $this->connection($this->defaultConnectionName); - } - - /** - * Check if default connection to RCON server is established. - * - * @return bool - */ - public function isConnected() - { - if (! array_key_exists($this->defaultConnectionName, $this->connections)) { - return false; - } - - return $this->defaultConnection() - ->isConnected(); - } - - /** - * Sends packet to default connection. - * - * @param int $id - * @param int $type - * @param string $body - * @return Packet - */ - public function send($id, $type, $body) - { - return $this->defaultConnection() - ->send($id, $type, $body); - } - - /** - * Check if connection default is authorized. - * - * @return bool - */ - public function isAuthorized() - { - if (! $this->isConnected()) { - return false; - } - - return $this->defaultConnection() - ->isAuthorized(); - } - - /** - * Authorize default connection with given password. - * - * @param string $password - * @return bool - */ - public function authorize($password) - { - return $this->defaultConnection() - ->authorize($password); - } - - /** - * Execute given command on default RCON server. - * - * @param string $command - * @return string - * @throws Exception - */ - public function command($command) - { - return $this->defaultConnection() - ->command($command); - } - - public function getPlayers($rcon) - { - try { - $result = $this->connection($rcon)->command('showPlayers'); - //First split return text by lines, then by commas - $lines = explode("\n", $result); - $players = []; - array_shift($lines); - foreach ($lines as $line) { - if(empty($line)) - break; - - $playerData = explode(",", $line); - $players[] = [ - 'name' => $playerData[0], - 'player_id' => $playerData[1], - 'steam_id' => $playerData[2] - ]; - } - $servers = Server::where('rcon', $rcon)->get(); - foreach ($servers as $server) { - $server->update(['online' => true]); - } - return $players; - } catch (Exception $ex) { - return []; - } - } - - public function shutdownServer($rcon) - { - $this->connection($rcon)->command('broadcast Restart_triggered_('.\Auth::user()->name.')'); - $this->connection($rcon)->command('save'); - $this->connection($rcon)->command('shutdown 20'); - $servers = Server::where('rcon', $rcon)->get(); - foreach ($servers as $server) - { - $server->update(['shutting_down' => true]); - } - } - - public function kickPlayer($rcon, $player_id) - { - $this->connection($rcon)->command('KickPlayer '.$player_id); - } - - public function banPlayer($rcon, $player_id) - { - $this->connection($rcon)->command('BanPlayer '.$player_id); - } -} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..969f1d3 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use App\Gameserver\Communication\Rcon; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -19,6 +20,8 @@ public function register(): void */ public function boot(): void { - // + $this->app->bind('Rcon', function() { + return new Rcon(); + }); } } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index e49d0b1..1972a82 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -2,7 +2,7 @@ namespace App\Providers; -use App\Listeners\NewUserListener; +use App\Listeners\UserHasRegistered; use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Listeners\SendEmailVerificationNotification; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; @@ -18,7 +18,7 @@ class EventServiceProvider extends ServiceProvider protected $listen = [ Registered::class => [ SendEmailVerificationNotification::class, - NewUserListener::class, + UserHasRegistered::class, ], ]; diff --git a/app/Providers/RCONServiceProvider.php b/app/Providers/RCONServiceProvider.php deleted file mode 100644 index b455a60..0000000 --- a/app/Providers/RCONServiceProvider.php +++ /dev/null @@ -1,50 +0,0 @@ -app->bind('RCON', function () { - return new RCON(); - }); - } - - /** - * Register bindings in the container. - * - * @return void - */ - public function register() - { - - } - - /** - * Get the services provided by the provider. - * - * @return array - */ - public function provides() - { - return [ - RCON::class - ]; - } -} diff --git a/app/Support/RCON/PalworldRcon.php b/app/Support/RCON/PalworldRcon.php new file mode 100644 index 0000000..ed2a069 --- /dev/null +++ b/app/Support/RCON/PalworldRcon.php @@ -0,0 +1,23 @@ +connection = new PalworldRconConnection($host, $port, $password, $timeout); + } + + public function command(string $command) : string + { + try { + $this->connection->sendCommand($command); + return $this->connection->getResponse(); + } catch (\Exception $exception) { + return $exception->getMessage(); + } + } +} diff --git a/app/Support/RCON/PalworldRconConnection.php b/app/Support/RCON/PalworldRconConnection.php new file mode 100644 index 0000000..7fe3f3c --- /dev/null +++ b/app/Support/RCON/PalworldRconConnection.php @@ -0,0 +1,101 @@ +isConnected()) + $this->connect(); + + if($this->password) + $this->authorize(); + + if(!$this->isAuthorized()) + { + $this->disconnect(); + throw new \Exception("Authorization rejected", 0); + } + + $this->sendPacket(PalworldRconPacket::ID_COMMAND, PalworldRconPacket::TYPE_SERVERDATA_EXECCOMMAND, $command); + } + + public function getResponse() + { + $response = $this->receivePacket()->getBody(); + $this->disconnect(); + return $response; + } + + public function sendPacket(int $id, int $type, string $body) + { + $packet = PalworldRconPacket::fromFields(['id' => $id, 'type' => $type, 'body' => $body]); + fwrite($this->socket, $packet->getBytes()); + } + + public function receivePacket() + { + $bytes = fread($this->socket, 4); + $size = unpack('V1size', $bytes); + + $bytes .= fread($this->socket, $size['size']); + + return PalworldRconPacket::fromBytes($bytes); + } + + public function authorize() + { + $this->sendPacket(PalworldRconPacket::ID_AUTHORIZE, PalworldRconPacket::TYPE_SERVERDATA_AUTH, $this->password); + $response = $this->receivePacket(); + if ($response->getId() == PalworldRconPacket::ID_AUTHORIZE && + $response->getType() == PalworldRconPacket::TYPE_SERVERDATA_AUTH_RESPONSE) { + $this->authorized = true; + } + + return $this->isAuthorized(); + } + + public function isAuthorized() + { + return $this->authorized; + } + + public function isConnected() : bool + { + return is_resource($this->socket); + } + + public function disconnect() : void + { + if(is_resource($this->socket)) + fclose($this->socket); + } + private function connect() : void + { + $this->socket = fsockopen($this->host, $this->port, $errorNumber, $errorMessage, $this->timeout); + + if(!$this->socket) + throw new \Exception("Could not connect to RCON[$this->host]:[$this->port]: ".$errorMessage, $errorNumber); + } +} diff --git a/app/PalWorld/RCON/Packet.php b/app/Support/RCON/PalworldRconPacket.php similarity index 88% rename from app/PalWorld/RCON/Packet.php rename to app/Support/RCON/PalworldRconPacket.php index d7b36a3..7a675e8 100644 --- a/app/PalWorld/RCON/Packet.php +++ b/app/Support/RCON/PalworldRconPacket.php @@ -1,8 +1,8 @@ fillable as $field) { - if (! array_key_exists($field, $fields)) { - throw new Exception("Invalid packet structure - missing $field field"); - } - - $this->{$field} = $fields[$field]; + $this->fillable[$field] = $fields[$field]; } $this->encode(); @@ -137,7 +133,7 @@ public function getSize() */ public function getId() { - return $this->id; + return $this->fillable['id']; } /** @@ -147,7 +143,7 @@ public function getId() */ public function getType() { - return $this->type; + return $this->fillable['type']; } /** @@ -157,7 +153,7 @@ public function getType() */ public function getBody() { - return $this->body; + return $this->fillable['body']; } /** @@ -168,7 +164,7 @@ public function getBody() */ protected function encode() { - $bytes = pack("VVZ*", $this->id, $this->type, $this->body); + $bytes = pack("VVZ*", $this->fillable['id'], $this->fillable['type'], $this->fillable['body']); $bytes .= "\x00"; $this->size = strlen($bytes); diff --git a/composer.json b/composer.json index 18084ee..65ac1fc 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "laravel/tinker": "^2.8", "livewire/livewire": "^3.0", "livewire/volt": "^1.0", + "masmerise/livewire-toaster": "^2.1", "rawilk/laravel-form-components": "^8.1", "spatie/laravel-permission": "^6.3", "spatie/laravel-short-schedule": "^1.5" diff --git a/composer.lock b/composer.lock index 9a523ee..cfc47a2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ea895cfcb3ed4fbcf65a76ffc616fa37", + "content-hash": "336d3849014b2374988d95fe4e672e9f", "packages": [ { "name": "brick/math", @@ -2040,6 +2040,77 @@ }, "time": "2024-01-03T14:09:47+00:00" }, + { + "name": "masmerise/livewire-toaster", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/masmerise/livewire-toaster.git", + "reference": "35104bbb930d2638335e3685a307fb0ec348f5e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/masmerise/livewire-toaster/zipball/35104bbb930d2638335e3685a307fb0ec348f5e5", + "reference": "35104bbb930d2638335e3685a307fb0ec348f5e5", + "shasum": "" + }, + "require": { + "laravel/framework": "^10.0", + "livewire/livewire": "^3.0", + "php": "~8.3" + }, + "conflict": { + "stevebauman/unfinalize": "*" + }, + "require-dev": { + "dive-be/php-crowbar": "^1.0", + "laravel/pint": "^1.0", + "nunomaduro/larastan": "^2.0", + "orchestra/testbench": "^8.0", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Toaster": "Masmerise\\Toaster\\Toaster" + }, + "providers": [ + "Masmerise\\Toaster\\ToasterServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Masmerise\\Toaster\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Muhammed Sari", + "email": "support@muhammedsari.me", + "role": "Developer" + } + ], + "description": "Beautiful toast notifications for Laravel / Livewire.", + "homepage": "https://github.com/masmerise/livewire-toaster", + "keywords": [ + "alert", + "laravel", + "livewire", + "toast", + "toaster" + ], + "support": { + "issues": "https://github.com/masmerise/livewire-toaster/issues", + "source": "https://github.com/masmerise/livewire-toaster/tree/2.1.0" + }, + "time": "2023-12-13T12:33:45+00:00" + }, { "name": "monolog/monolog", "version": "3.5.0", diff --git a/config/app.php b/config/app.php index 57416dd..c8b7e0a 100644 --- a/config/app.php +++ b/config/app.php @@ -168,8 +168,7 @@ // App\Providers\BroadcastServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\RouteServiceProvider::class, - App\Providers\VoltServiceProvider::class, - App\Providers\RCONServiceProvider::class + App\Providers\VoltServiceProvider::class ])->toArray(), /* @@ -185,6 +184,6 @@ 'aliases' => Facade::defaultAliases()->merge([ // 'Example' => App\Facades\Example::class, - 'RCON' => App\PalWorld\RCON\Facades\Facade::class, + 'Rcon' => App\Facades\Rcon::class, ])->toArray(), ]; diff --git a/config/database.php b/config/database.php index 137ad18..fdeafaf 100644 --- a/config/database.php +++ b/config/database.php @@ -57,7 +57,7 @@ 'prefix' => '', 'prefix_indexes' => true, 'strict' => true, - 'engine' => null, + 'engine' => 'InnoDB', 'options' => extension_loaded('pdo_mysql') ? array_filter([ PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), ]) : [], diff --git a/config/models.php b/config/models.php index 8e4d387..85eeb49 100644 --- a/config/models.php +++ b/config/models.php @@ -447,7 +447,7 @@ | NOTE: This requires PHP 7.0 or later. | */ - 'enable_return_types' => false, + 'enable_return_types' => true, ], /* diff --git a/config/rcon.php b/config/rcon.php deleted file mode 100644 index dca1c3d..0000000 --- a/config/rcon.php +++ /dev/null @@ -1,41 +0,0 @@ - 'default', - - /* - |-------------------------------------------------------------------------- - | RCON Connections - |-------------------------------------------------------------------------- - | - | Here are each of the RCON connections setup for your application. - | - */ - - 'connections' => [ - 'default' => [ - 'host' => 'someHost', - 'port' => 123, - 'password' => 'SuperSafePassword', - 'timeout' => 60 - ], - 'default2' => [ - 'host' => 'someHost', - 'port' => 123, - 'password' => 'SuperSafePassword', - 'timeout' => 60 - ], - ] - -]; diff --git a/config/toaster.php b/config/toaster.php new file mode 100644 index 0000000..b257694 --- /dev/null +++ b/config/toaster.php @@ -0,0 +1,46 @@ + true, + + /** + * The vertical alignment of the toast container. + * + * Supported: "bottom", "middle" or "top" + */ + 'alignment' => 'bottom', + + /** + * Allow users to close toast messages prematurely. + * + * Supported: true | false + */ + 'closeable' => true, + + /** + * The on-screen duration of each toast. + * + * Minimum: 3000 (in milliseconds) + */ + 'duration' => 3000, + + /** + * The horizontal position of each toast. + * + * Supported: "center", "left" or "right" + */ + 'position' => 'right', + + /** + * Whether messages passed as translation keys should be translated automatically. + * + * Supported: true | false + */ + 'translate' => true, +]; diff --git a/database/migrations/2024_02_03_224620_rename_join_and_leave_table.php b/database/migrations/2024_02_03_224620_rename_join_and_leave_table.php new file mode 100644 index 0000000..ee41150 --- /dev/null +++ b/database/migrations/2024_02_03_224620_rename_join_and_leave_table.php @@ -0,0 +1,24 @@ +id(); + $table->string('host'); + $table->integer('port', false, true); + $table->string('password')->nullable(); + $table->integer('timeout')->default(5); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('rcon_data'); + } +}; diff --git a/database/migrations/2024_02_04_051514_add_rcon_i_dto_servers_table.php b/database/migrations/2024_02_04_051514_add_rcon_i_dto_servers_table.php new file mode 100644 index 0000000..f124ce5 --- /dev/null +++ b/database/migrations/2024_02_04_051514_add_rcon_i_dto_servers_table.php @@ -0,0 +1,33 @@ +dropColumn('rcon'); + + $table->foreignId('rcon_data_id'); + + $table->foreign('rcon_data_id')->on('rcon_data')->references('id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->dropForeign(['rcon_data_id']); + $table->dropColumn('rcon_data_id'); + }); + } +}; diff --git a/docker/8.3/supervisord.conf b/docker/8.3/supervisord.conf index c199a6b..488b652 100644 --- a/docker/8.3/supervisord.conf +++ b/docker/8.3/supervisord.conf @@ -14,10 +14,12 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 [program:scheduler] -command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan short-schedule:run --lifetime=60 +command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan short-schedule:run --lifetime=300 user=sail environment=LARAVEL_SAIL="1" stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 +autostart=true +autorestart=true diff --git a/install-start.ps1 b/install-start.ps1 new file mode 100644 index 0000000..fb3654b --- /dev/null +++ b/install-start.ps1 @@ -0,0 +1,82 @@ +$PHP_INSTALL_DIR = 'C:\php' +$PHP_IN_PATH = 0 +$PHP_8_3_URL = "https://windows.php.net/downloads/releases/php-8.3.2-Win32-vs16-x64.zip" +$DL_DEST = "C:\php\php.zip" +$PHP_CONFIG_PATH = "C:\php\php.ini" +$PHP_PROD_CONFIG_PATH = "C:\php\php.ini-production" +$PHP_PATH="php.exe" +$SCRIPT_PATH = split-path -parent $MyInvocation.MyCommand.Definition +$DB_PATH = "$SCRIPT_PATH\database\database.sqlite" + + + + +#Check if php.exe is already in $PATH +if ((Get-Command $PHP_PATH -ErrorAction SilentlyContinue) -eq $null) +{ + Write-Output "PHP is not in PATH" + #Check if a path for php already exists + if (Test-Path -Path $PHP_INSTALL_DIR) { + Write-Output "PHP Path exists, skipping dowload" + } else { + New-Item -ItemType Directory -Path C:\php + Invoke-WebRequest -Uri $PHP_8_3_URL -OutFile $DL_DEST + Expand-Archive -LiteralPath $DL_DEST -DestinationPath $PHP_INSTALL_DIR + $PHP_PATH = C:\php\php.exe + } + + $PHP_PATH = 'C:\php\php.exe' + + if(Test-Path $PHP_CONFIG_PATH -PathType Leaf) + { + Write-Output "PHP ini exists, skipping extension activation" + } else { + $content = Get-Content $PHP_PROD_CONFIG_PATH + $content | ForEach-Object {$_ -replace ";extension=curl", "extension=curl" -replace ";extension=fileinfo", "extension=fileinfo" -replace ";extension=mbstring", "extension=mbstring" -replace ";extension=openssl", "extension=openssl" -replace ";extension=pdo_mysql", "extension=pdo_mysql" -replace ";extension=pdo_sqlite", "extension=pdo_sqlite"} | Set-Content $PHP_CONFIG_PATH + } +} + +Get-Content "$SCRIPT_PATH\.env" | Where { $_ } | foreach { + $name, $value = $_.split('=') + + if ([string]::IsNullOrWhiteSpace($name) -or $name.Contains('#')) { + continue + } + + if($name.Equals("APP_KEY") -and [string]::IsNullOrWhiteSpace($value)) + { + Write-Output "Generating App Key" + & "$PHP_PATH" @("$SCRIPT_PATH\artisan", 'key:generate', '--force') + } +} + +& "$PHP_PATH" @("$SCRIPT_PATH\artisan", 'config:clear') +& "$PHP_PATH" @("$SCRIPT_PATH\artisan", 'cache:clear') +$content = Get-Content "$SCRIPT_PATH\.env" +$content | ForEach-Object {$_ -replace "DB_CONNECTION=mysql", "DB_CONNECTION=sqlite" -replace "DB_DATABASE=palworld_admin_panel", ""} | Set-Content "$SCRIPT_PATH\.env" + +if(Test-Path $DB_PATH -PathType Leaf) +{ + Write-Output "DB File Exists." +} +else +{ + Write-Output "Creating new file" + New-Item -type file "$DB_PATH" +} +Write-Output ".env Adjusted" + +#Onetime +Start-Process -NoNewWindow -FilePath "$PHP_PATH" -ArgumentList "$SCRIPT_PATH\artisan", "serve", "--host=127.0.0.1", "--port=80" +Start-Process -NoNewWindow -FilePath "$PHP_PATH" -ArgumentList "$SCRIPT_PATH\artisan", "short-schedule:run" + +#New-Service -Name "Palworld Admin Tool WebService" -BinaryPathName "$PHP_PATH $SCRIPT_PATH\artisan serve --host=127.0.0.1 --port=80" +#New-Service -Name "Palworld Admin Tool Scheduler" -BinaryPathName "$PHP_PATH $SCRIPT_PATH\artisan short-schedule:run" + +#Service Part +#Start-Process -FilePath powershell.exe -Verb RunAs -Wait -ArgumentList '-Command', "function myService() {New-Service -Name `"Palworld Admin Tool WebService`" -BinaryPathName `"$PHP_PATH $SCRIPT_PATH\artisan serve --host=127.0.0.1 --port=80`"};myService" +#Start-Process -FilePath powershell.exe -Verb RunAs -Wait -ArgumentList '-Command', "function myService() {New-Service -Name `"Palworld Admin Tool Scheduler`" -BinaryPathName `"$PHP_PATH $SCRIPT_PATH\artisan short-schedule:run`"};myService" + + +#Start-Service -Name "Palworld Admin Tool WebService" +#Start-Service -Name "Palworld Admin Tool Scheduler" diff --git a/package-lock.json b/package-lock.json index ba49794..40d8ccc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,13 @@ { - "name": "palworld-admin-panel", + "name": "html", "lockfileVersion": 2, "requires": true, "packages": { "": { + "dependencies": { + "moment": "^2.30.1", + "moment-timezone": "^0.5.45" + }, "devDependencies": { "@tailwindcss/forms": "^0.5.2", "autoprefixer": "^10.4.2", @@ -1477,6 +1481,25 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.45", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz", + "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -3247,6 +3270,19 @@ "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", "dev": true }, + "moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==" + }, + "moment-timezone": { + "version": "0.5.45", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz", + "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==", + "requires": { + "moment": "^2.29.4" + } + }, "mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", diff --git a/package.json b/package.json index 0b3bd87..369e190 100644 --- a/package.json +++ b/package.json @@ -13,5 +13,9 @@ "postcss": "^8.4.31", "tailwindcss": "^3.1.0", "vite": "^5.0.0" + }, + "dependencies": { + "moment": "^2.30.1", + "moment-timezone": "^0.5.45" } } diff --git a/resources/js/app.js b/resources/js/app.js index e59d6a0..b6223ea 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1 +1,2 @@ import './bootstrap'; +import '../../vendor/masmerise/livewire-toaster/resources/js'; diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js index 846d350..40d5c82 100644 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -30,3 +30,6 @@ window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; // forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https', // enabledTransports: ['ws', 'wss'], // }); + +import moment from 'moment-timezone'; +window.moment = moment diff --git a/resources/views/components/link-danger-button.blade.php b/resources/views/components/link-danger-button.blade.php new file mode 100644 index 0000000..19826ae --- /dev/null +++ b/resources/views/components/link-danger-button.blade.php @@ -0,0 +1,3 @@ +merge(['href' => '#', 'class' => 'inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}> + {{ $slot }} + diff --git a/resources/views/components/link-primary-button.blade.php b/resources/views/components/link-primary-button.blade.php new file mode 100644 index 0000000..2600b9c --- /dev/null +++ b/resources/views/components/link-primary-button.blade.php @@ -0,0 +1,3 @@ +merge(['href' => '#', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}> + {{ $slot }} + diff --git a/resources/views/components/link-secondary-button.blade.php b/resources/views/components/link-secondary-button.blade.php new file mode 100644 index 0000000..ad1f87d --- /dev/null +++ b/resources/views/components/link-secondary-button.blade.php @@ -0,0 +1,3 @@ +merge(['href' => '#', 'class' => 'inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150']) }}> + {{ $slot }} + diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index a227d0e..3007876 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -32,6 +32,7 @@ {{ $slot }} + diff --git a/resources/views/livewire/add-rcon.blade.php b/resources/views/livewire/add-rcon.blade.php new file mode 100644 index 0000000..bed20e1 --- /dev/null +++ b/resources/views/livewire/add-rcon.blade.php @@ -0,0 +1,62 @@ +
+ +

+ {{ __('Add RCON Connection') }} +

+ +
+
+
+
+
+
+
+ +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + + {{ __('Test Connection') }} + + + +
+ + +
+ +
+ @if($testSuccessful) + + {{ __('Add Rcon') }} + + @else + + {{ __('Please Test the connection first') }} + + @endif +
+
+
+
+
+
+
+
diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php index 7aeae32..afa6383 100644 --- a/resources/views/livewire/dashboard.blade.php +++ b/resources/views/livewire/dashboard.blade.php @@ -10,7 +10,15 @@
- No what to put here yet. +

Here a Quick Introduction to Palworld Server Manager.

+ +

1.) Navigate to your top right corner and select 'RCON Connections'

+ +

2.) Click "Add RCON"

+ +

3.) Fill in the Connection Details as configured on your PalWorld Server and Test the connection, if the Test is successful you can now click "Add RCON"

+ +

4.) Unless you did not un-tick "Quick Create Server" all the necessary information will be pulled from your Gameserver and you can enjoy the tool.

diff --git a/resources/views/livewire/edit-rcon.blade.php b/resources/views/livewire/edit-rcon.blade.php new file mode 100644 index 0000000..69eadf4 --- /dev/null +++ b/resources/views/livewire/edit-rcon.blade.php @@ -0,0 +1,62 @@ +
+ +

+ {{ __('Edit RCON Connection') }} +

+ +
+
+
+
+
+
+
+ +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + + {{ __('Test Connection') }} + + + +
+ + +
+ +
+ @if($testSuccessful) + + {{ __('Edit Rcon') }} + + @else + + {{ __('Please Test the connection first') }} + + @endif +
+
+
+
+
+
+
+
diff --git a/resources/views/livewire/edit-whitelist.blade.php b/resources/views/livewire/edit-whitelist.blade.php index ac4bf69..b938975 100644 --- a/resources/views/livewire/edit-whitelist.blade.php +++ b/resources/views/livewire/edit-whitelist.blade.php @@ -54,9 +54,9 @@
- +
@@ -78,9 +78,9 @@
- +
diff --git a/resources/views/livewire/installer.blade.php b/resources/views/livewire/installer.blade.php index 9bb15f2..8ba3701 100644 --- a/resources/views/livewire/installer.blade.php +++ b/resources/views/livewire/installer.blade.php @@ -85,6 +85,6 @@

@endif @if(!$noStartInstallation) - Install + Install @endif diff --git a/resources/views/livewire/layout/navigation.blade.php b/resources/views/livewire/layout/navigation.blade.php index 3ac718d..b0873bf 100644 --- a/resources/views/livewire/layout/navigation.blade.php +++ b/resources/views/livewire/layout/navigation.blade.php @@ -88,6 +88,9 @@ public function logout(Logout $logout): void {{ __('Server Overview') }} + + {{ __('Rcon Connections') }} + - + @else Player Offline! @@ -99,21 +99,31 @@ {{ __('Join/Leave log of Server ').$serverName }}
-
+
@foreach($joinLeaveLog as $log) -

- {{ $log->created_at }} Player +

+ Player {{ $log->player->name.'('.$log->player->player_id.'/'.$log->player->steam_id.')' }} @if($log->action == 'JOIN') joined @elseif($log->action == 'LEFT') left @elseif($log->action == 'KICKWL') - was kicked by the automatic Whitelist from + was kicked by the automatic Whitelist from @elseif($log->action == 'KICKUSR') - was kicked by a User from + was kicked by a User from @elseif($log->action == 'BANUSR') - was banned by a User from + was banned by a User from @endif the Server.

@@ -121,7 +131,6 @@
- diff --git a/resources/views/livewire/server-overview.blade.php b/resources/views/livewire/server-overview.blade.php index 3bd4920..c03b251 100644 --- a/resources/views/livewire/server-overview.blade.php +++ b/resources/views/livewire/server-overview.blade.php @@ -45,11 +45,11 @@ {{ $server->id }} - + {{ $server->name }} - {{ $server->rcon }} + {{ $server->rconData->host.':'.$server->rconData->port }} @if($server->uses_whitelist) @@ -121,19 +121,19 @@
- + Edit - - - + + + Delete Server +
@@ -141,7 +141,7 @@
- Add Server + Add Server
diff --git a/routes/web.php b/routes/web.php index d19b739..504b852 100644 --- a/routes/web.php +++ b/routes/web.php @@ -42,6 +42,18 @@ ->middleware(['auth', 'verified']) ->name('server-overview'); + Route::get('rcon', \App\Livewire\RconOverview::class) + ->middleware(['auth', 'verified']) + ->name('rcon-overview'); + + Route::get('rcon/add', \App\Livewire\AddRcon::class) + ->middleware(['auth', 'verified']) + ->name('add-rcon'); + + Route::get('rcon/edit/{id}', \App\Livewire\EditRcon::class) + ->middleware(['auth', 'verified']) + ->name('edit-rcon'); + Route::get('server/add', \App\Livewire\AddServer::class) ->middleware(['auth', 'verified']) ->name('add-server'); diff --git a/tailwind.config.js b/tailwind.config.js index c29eb1a..85a9d1f 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -7,6 +7,7 @@ export default { './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', './storage/framework/views/*.php', './resources/views/**/*.blade.php', + './vendor/masmerise/livewire-toaster/resources/views/*.blade.php', ], theme: {