diff --git a/app/Http/Controllers/Server/AuthController.php b/app/Http/Controllers/Server/AuthController.php index 11fb010b73dae1b1470d1f7390c40471d1efc854..eb7dd883e8e2499b2745311f5507635bb777f2dc 100644 --- a/app/Http/Controllers/Server/AuthController.php +++ b/app/Http/Controllers/Server/AuthController.php @@ -176,4 +176,37 @@ public function heartbeat(ServerHeartbeatRequest $request) $server->heartbeat($request->input("ip"), $request->input("port"), $request->input("pubkey")); return response()->noContent(); } + + /** + * @OA\Post( + * tags={"Servers:Authentication"}, + * path="/auth/server/stop", + * summary="Singla to the API that the server has stopped", + * @OA\Response( + * response="401", + * ref="#/components/responses/401" + * ), + * @OA\Response( + * response="403", + * description="Server is offline", + * ), + * @OA\Response( + * response="204", + * description="OK", + * ), + * ) + */ + public function stop(Request $request) + { + $server = Auth::guard('server')->user(); + + if (!$server->online) { + return response()->json([ + 'message' => 'Server is offline', + ], 403); + } + + $server->stop(); + return response()->noContent(); + } } diff --git a/app/Jobs/CheckServers.php b/app/Jobs/CheckServers.php index 2b0db6a20f5888749eef67b9a069ed39fa3f730d..35dfedc410e9e40f5f249f1ae6641b8034b6ba10 100644 --- a/app/Jobs/CheckServers.php +++ b/app/Jobs/CheckServers.php @@ -35,22 +35,7 @@ public function handle() // We grab the server that are marked online but didn't do a heartbeat in the last two minutes. // This happens when a server crashes or goes offline. Server::where("online", true)->where('last_heartbeat_at', '<', Carbon::now()->subSeconds(120)->toDateTimeString())->lazyById()->each(function ($server) { - // We fetch all the users connected to the server and mark them as disconnected. - // NOTE: The users will have to wait at least one minute and at most three minutes - // since the server crashes before connecting to another server. - $server->users->each(function ($user) { - $user->disconnect(); - }); - - // We fetch all the game that were running, detach their users (thus deleting the pivot data) - // and delete the game. This makes it as the game never happened. - $server->games->whereIn("state", ["created", "playing"])->each(function($game) { - $game->users()->detach(); - $game->delete(); - }); - - // We mark the server as offline - $server->offline(); + $server->stop(); }); } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 27613dec705dea4235d398c10a751d7ec8c388e1..cd2355a19e909c1f264176d1efe59f046a62decf 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -113,4 +113,24 @@ public function offline() $this->pubkey = null; $this->save(); } + + public function stop() + { + // We fetch all the users connected to the server and mark them as disconnected. + // NOTE: The users will have to wait at least one minute and at most three minutes + // since the server crashes before connecting to another server. + $this->users->each(function ($user) { + $user->disconnect(); + }); + + // We fetch all the game that were running, detach their users (thus deleting the pivot data) + // and delete the game. This makes it as the game never happened. + $this->games->whereIn("state", ["created", "playing"])->each(function($game) { + $game->users()->detach(); + $game->delete(); + }); + + // We mark the server as offline + $this->offline(); + } } \ No newline at end of file diff --git a/composer.json b/composer.json index e4e67a6ab4e210f3382f11b13caa627f5083011f..dcfca6f3ae0d009b7bf4dfe05c8e31c6d1173abd 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "description": "The Laravel Framework.", "keywords": ["framework", "laravel"], "license": "MIT", - "version": "1.1.0", + "version": "1.2.0", "require": { "php": "^8.0.2", "darkaonline/l5-swagger": "^8.4", diff --git a/docker-compose.yml b/docker-compose.yml index 3f2d7449b7b540f2d82ac832213731bb87e165e0..e663b75a265784396f5221bb468499742b4453c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,7 @@ services: database: condition: service_healthy build: . - command: sh -c "php artisan migrate --force && php artisan serve --host 0.0.0.0" + command: sh -c "php artisan migrate --force && php artisan l5-swagger:generate && php artisan serve --host 0.0.0.0" image: registry.app.unistra.fr/bombernyan/api volumes: - storage:/api/storage diff --git a/routes/api.php b/routes/api.php index 54c49774a750f9704e90e97f5d1a8a800a004012..778f260e0e10c7eb37af506080073a13517cc3b7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -38,6 +38,7 @@ Route::post('refresh', [Server\AuthController::class, 'refresh']); Route::get('profile', [Server\AuthController::class, 'profile']); Route::post('heartbeat', [Server\AuthController::class, 'heartbeat']); + Route::post('stop', [Server\AuthController::class, 'stop']); }); }); diff --git a/tests/Feature/StopTest.php b/tests/Feature/StopTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d23042dda52a147e0d23ea78b13e6dd0e7304671 --- /dev/null +++ b/tests/Feature/StopTest.php @@ -0,0 +1,190 @@ +<?php + +namespace Tests\Feature; + +use App\Jobs\CheckServers; +use App\Models\Game; +use App\Models\Server; +use App\Models\User; +use Carbon\Carbon; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Foundation\Testing\WithFaker; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; +use Tests\TestCase; + +class StopTest extends TestCase +{ + use RefreshDatabase; + + public function test_stop() + { + $user = User::create([ + 'username' => 'test', + 'email' => 'test@localhost', + 'password' => Hash::make('test'), + ]); + $user2 = User::create([ + 'username' => 'test2', + 'email' => 'test2@localhost', + 'password' => Hash::make('test'), + ]); + + $server = Server::create([ + 'login' => 'test', + 'password' => Hash::make('test'), + 'owner_id' => $user->id, + 'public' => true + ]); + + $user->server_id = $server->id; + $user->server_state = 'connected'; + $user->server_token = null; + $user->save(); + + $user2->server_id = $server->id; + $user2->server_state = 'connected'; + $user2->server_token = null; + $user2->save(); + + $server->heartbeat('127.0.0.1', 12345, 'pubkey'); + $server->last_heartbeat_at = Carbon::now()->subMinutes(5); + $server->save(); + + $game = Game::create([ + "server_id" => $server->id + ]); + + $game->users()->attach($user->id); + $game->users()->attach($user2->id); + + $server = $server->refresh(); + $this->assertEquals(true, $server->online); + $this->assertEquals("127.0.0.1", $server->ip); + $this->assertEquals(12345, $server->port); + $this->assertEquals("pubkey", $server->pubkey); + + $server_token = Auth::guard('server')->login($server); + + $response = $this->withHeaders([ + "Authorization" => "Bearer " . $server_token + ])->post('/api/auth/server/stop'); + $response->assertStatus(204); + + $server = $server->refresh(); + $this->assertEquals(null, $server->online); + $this->assertEquals(null, $server->ip); + $this->assertEquals(null, $server->port); + $this->assertEquals(null, $server->pubkey); + + $user = $user->refresh(); + $this->assertEquals(null, $user->server_id); + $this->assertEquals("offline", $user->server_state); + $this->assertEquals(null, $user->server_token); + $user2 = $user2->refresh(); + $this->assertEquals(null, $user2->server_id); + $this->assertEquals("offline", $user2->server_state); + $this->assertEquals(null, $user2->server_token); + + $this->assertDatabaseCount("games", 0); + $this->assertDatabaseCount("users_games", 0); + } + + public function test_stop_game_finished() + { + $user = User::create([ + 'username' => 'test', + 'email' => 'test@localhost', + 'password' => Hash::make('test'), + ]); + $user2 = User::create([ + 'username' => 'test2', + 'email' => 'test2@localhost', + 'password' => Hash::make('test'), + ]); + + $server = Server::create([ + 'login' => 'test', + 'password' => Hash::make('test'), + 'owner_id' => $user->id, + 'public' => true + ]); + + $user->server_id = $server->id; + $user->server_state = 'connected'; + $user->server_token = null; + $user->save(); + + $user2->server_id = $server->id; + $user2->server_state = 'connected'; + $user2->server_token = null; + $user2->save(); + + $server->heartbeat('127.0.0.1', 12345, 'pubkey'); + $server->last_heartbeat_at = Carbon::now()->subMinutes(5); + $server->save(); + + $game = Game::create([ + "server_id" => $server->id + ]); + + $game->users()->attach($user->id); + $game->users()->attach($user2->id); + $game->state = 'finished'; + $game->save(); + + $server = $server->refresh(); + $this->assertEquals(true, $server->online); + $this->assertEquals("127.0.0.1", $server->ip); + $this->assertEquals(12345, $server->port); + $this->assertEquals("pubkey", $server->pubkey); + + $server_token = Auth::guard('server')->login($server); + + $response = $this->withHeaders([ + "Authorization" => "Bearer " . $server_token + ])->post('/api/auth/server/stop'); + $response->assertStatus(204); + + $server = $server->refresh(); + $this->assertEquals(null, $server->online); + $this->assertEquals(null, $server->ip); + $this->assertEquals(null, $server->port); + $this->assertEquals(null, $server->pubkey); + + $user = $user->refresh(); + $this->assertEquals(null, $user->server_id); + $this->assertEquals("offline", $user->server_state); + $this->assertEquals(null, $user->server_token); + $user2 = $user2->refresh(); + $this->assertEquals(null, $user2->server_id); + $this->assertEquals("offline", $user2->server_state); + $this->assertEquals(null, $user2->server_token); + + $this->assertDatabaseCount("games", 1); + $this->assertDatabaseCount("users_games", 2); + } + + public function test_stop_offline() + { + $user = User::create([ + 'username' => 'test', + 'email' => 'test@localhost', + 'password' => Hash::make('test'), + ]); + + $server = Server::create([ + 'login' => 'test', + 'password' => Hash::make('test'), + 'owner_id' => $user->id, + 'public' => true + ]); + + $server_token = Auth::guard('server')->login($server); + + $response = $this->withHeaders([ + "Authorization" => "Bearer " . $server_token + ])->post('/api/auth/server/stop'); + $response->assertStatus(403); + } +}