mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 00:02:15 +01:00
add back destroy other browser sessions endpoint (jetstream migration)
This commit is contained in:
committed by
Constantin Graf
parent
5dcd8197dc
commit
f9271664f0
54
app/Http/Controllers/Web/OtherBrowserSessionsController.php
Normal file
54
app/Http/Controllers/Web/OtherBrowserSessionsController.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use Illuminate\Contracts\Auth\StatefulGuard;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Fortify\Actions\ConfirmPassword;
|
||||
|
||||
class OtherBrowserSessionsController extends Controller
|
||||
{
|
||||
/**
|
||||
* Log the user out of their other browser sessions across all devices.
|
||||
*/
|
||||
public function destroy(Request $request, StatefulGuard $guard): RedirectResponse
|
||||
{
|
||||
$password = (string) $request->string('password');
|
||||
|
||||
$confirmed = app(ConfirmPassword::class)($guard, $request->user(), $password);
|
||||
|
||||
if (! $confirmed) {
|
||||
throw ValidationException::withMessages([
|
||||
'password' => __('The password is incorrect.'),
|
||||
]);
|
||||
}
|
||||
|
||||
$guard->logoutOtherDevices($password);
|
||||
|
||||
$this->deleteOtherSessionRecords($request);
|
||||
|
||||
return back(303);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the other browser session records from storage.
|
||||
*/
|
||||
protected function deleteOtherSessionRecords(Request $request): void
|
||||
{
|
||||
if (config('session.driver') !== 'database') {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::connection(config('session.connection'))
|
||||
->table(config('session.table', 'sessions'))
|
||||
->where('user_id', $request->user()->getAuthIdentifier())
|
||||
->where('id', '!=', $request->session()->getId())
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use App\Http\Controllers\Web\DashboardController;
|
||||
use App\Http\Controllers\Web\HomeController;
|
||||
use App\Http\Controllers\Web\OrganizationController;
|
||||
use App\Http\Controllers\Web\OrganizationInvitationController;
|
||||
use App\Http\Controllers\Web\OtherBrowserSessionsController;
|
||||
use App\Http\Controllers\Web\UserController;
|
||||
use App\Http\Controllers\Web\UserProfileController;
|
||||
use App\Service\PermissionStore;
|
||||
@@ -102,6 +103,8 @@ Route::middleware([
|
||||
return to_route('organizations.show', [$organizationId]);
|
||||
})->name('teams.show');
|
||||
Route::get('/user/profile', [UserProfileController::class, 'show'])->name('profile.show');
|
||||
Route::delete('/user/other-browser-sessions', [OtherBrowserSessionsController::class, 'destroy'])
|
||||
->name('other-browser-sessions.destroy');
|
||||
});
|
||||
|
||||
Route::get('/team-invitations/{invitation}', [OrganizationInvitationController::class, 'accept'])
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class BrowserSessionsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_other_browser_sessions_can_be_logged_out(): void
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
$response = $this->delete('/user/other-browser-sessions', [
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasNoErrors();
|
||||
}
|
||||
}
|
||||
103
tests/Unit/Endpoint/Web/OtherBrowserSessionsEndpointTest.php
Normal file
103
tests/Unit/Endpoint/Web/OtherBrowserSessionsEndpointTest.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Endpoint\Web;
|
||||
|
||||
use App\Http\Controllers\Web\OtherBrowserSessionsController;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
|
||||
#[CoversClass(OtherBrowserSessionsController::class)]
|
||||
class OtherBrowserSessionsEndpointTest extends EndpointTestAbstract
|
||||
{
|
||||
public function test_destroy_logs_out_other_browser_sessions_with_the_correct_password(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = User::factory()->create();
|
||||
$originalPasswordHash = $user->password;
|
||||
$this->actingAs($user);
|
||||
|
||||
// Act
|
||||
$response = $this->delete('/user/other-browser-sessions', [
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHasNoErrors();
|
||||
// logoutOtherDevices re-hashes the password (same plaintext, new hash) to invalidate other sessions.
|
||||
$this->assertNotSame($originalPasswordHash, $user->fresh()->password);
|
||||
$this->assertTrue(Hash::check('password', $user->fresh()->password));
|
||||
}
|
||||
|
||||
public function test_destroy_fails_with_an_incorrect_password(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = User::factory()->create();
|
||||
$originalPasswordHash = $user->password;
|
||||
$this->actingAs($user);
|
||||
|
||||
// Act
|
||||
$response = $this->delete('/user/other-browser-sessions', [
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertSessionHasErrors('password');
|
||||
// No side effects when the password is incorrect: the password must not be re-hashed.
|
||||
$this->assertSame($originalPasswordHash, $user->fresh()->password);
|
||||
}
|
||||
|
||||
public function test_destroy_requires_authentication(): void
|
||||
{
|
||||
// Act
|
||||
$response = $this->delete('/user/other-browser-sessions', [
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertRedirect(route('login'));
|
||||
}
|
||||
|
||||
public function test_destroy_deletes_the_other_database_session_records_of_the_current_user(): void
|
||||
{
|
||||
// Arrange
|
||||
config(['session.driver' => 'database']);
|
||||
$user = User::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
DB::table('sessions')->insert([
|
||||
[
|
||||
'id' => 'other-session-of-current-user',
|
||||
'user_id' => $user->getKey(),
|
||||
'ip_address' => '192.0.2.10',
|
||||
'user_agent' => '',
|
||||
'payload' => '',
|
||||
'last_activity' => now()->subMinutes(5)->timestamp,
|
||||
],
|
||||
[
|
||||
'id' => 'session-of-another-user',
|
||||
'user_id' => $otherUser->getKey(),
|
||||
'ip_address' => '192.0.2.30',
|
||||
'user_agent' => '',
|
||||
'payload' => '',
|
||||
'last_activity' => now()->timestamp,
|
||||
],
|
||||
]);
|
||||
|
||||
// Act
|
||||
$response = $this->delete('/user/other-browser-sessions', [
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertSessionHasNoErrors();
|
||||
// The current user's other sessions are removed, while another user's session is untouched.
|
||||
$this->assertDatabaseMissing('sessions', ['id' => 'other-session-of-current-user']);
|
||||
$this->assertDatabaseHas('sessions', ['id' => 'session-of-another-user']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user