mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-13 10:42:16 +01:00
add back destroy other browser sessions endpoint (jetstream migration)
This commit is contained in:
committed by
Constantin Graf
parent
a4c400ef4e
commit
6b01034bdd
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\HomeController;
|
||||||
use App\Http\Controllers\Web\OrganizationController;
|
use App\Http\Controllers\Web\OrganizationController;
|
||||||
use App\Http\Controllers\Web\OrganizationInvitationController;
|
use App\Http\Controllers\Web\OrganizationInvitationController;
|
||||||
|
use App\Http\Controllers\Web\OtherBrowserSessionsController;
|
||||||
use App\Http\Controllers\Web\UserController;
|
use App\Http\Controllers\Web\UserController;
|
||||||
use App\Http\Controllers\Web\UserProfileController;
|
use App\Http\Controllers\Web\UserProfileController;
|
||||||
use App\Service\PermissionStore;
|
use App\Service\PermissionStore;
|
||||||
@@ -102,6 +103,8 @@ Route::middleware([
|
|||||||
return to_route('organizations.show', [$organizationId]);
|
return to_route('organizations.show', [$organizationId]);
|
||||||
})->name('teams.show');
|
})->name('teams.show');
|
||||||
Route::get('/user/profile', [UserProfileController::class, 'show'])->name('profile.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'])
|
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