diff --git a/config/routes.php b/config/routes.php index 831c137a..bbd092e5 100644 --- a/config/routes.php +++ b/config/routes.php @@ -41,6 +41,8 @@ $route->addGroup( $route->get('/certificates', 'SettingsController@ifsgCertificate'); $route->post('/certificates', 'SettingsController@saveIfsgCertificate'); $route->get('/oauth', 'SettingsController@oauth'); + $route->get('/sessions', 'SettingsController@sessions'); + $route->post('/sessions', 'SettingsController@sessionsDelete'); } ); diff --git a/resources/lang/de_DE/additional.po b/resources/lang/de_DE/additional.po index 094c6635..1ecebf9f 100644 --- a/resources/lang/de_DE/additional.po +++ b/resources/lang/de_DE/additional.po @@ -145,6 +145,9 @@ msgstr "Bitte gib Dein geplantes Abreisedatum an, damit wir ein Gefühl für die msgid "settings.profile.success" msgstr "Einstellungen gespeichert." +msgid "settings.sessions.delete_success" +msgstr "Sitzung erfolgreich gelöscht." + msgid "faq.delete.success" msgstr "FAQ Eintrag erfolgreich gelöscht." diff --git a/resources/lang/de_DE/default.po b/resources/lang/de_DE/default.po index faab84d7..d201f784 100644 --- a/resources/lang/de_DE/default.po +++ b/resources/lang/de_DE/default.po @@ -1122,6 +1122,9 @@ msgstr "Senden" msgid "Y-m-d H:i" msgstr "d.m.Y H:i" +msgid "Y-m-d H:i:s" +msgstr "d.m.Y H:i:s" + msgid "mark as read" msgstr "als gelesen markieren" @@ -1974,6 +1977,9 @@ msgstr "Vorschau" msgid "form.delete" msgstr "Löschen" +msgid "form.delete_all" +msgstr "Alle löschen" + msgid "form.updated" msgstr "Aktualisiert" @@ -2201,6 +2207,21 @@ msgstr "Passwort wiederholen" msgid "settings.password.success" msgstr "Passwort wurde erfolgreich geändert." +msgid "settings.sessions" +msgstr "Sitzungen" + +msgid "settings.sessions.info" +msgstr "Hier kannst Du deine Bowser-Sitzungen sehen und löschen." + +msgid "settings.sessions.current" +msgstr "Aktuelle Sitzung" + +msgid "settings.sessions.id" +msgstr "Sitzungs-ID" + +msgid "settings.sessions.last_activity" +msgstr "Zuletzt verwendet" + msgid "settings.theme" msgstr "Theme" diff --git a/resources/lang/en_US/additional.po b/resources/lang/en_US/additional.po index d72fdfa7..52adf89c 100644 --- a/resources/lang/en_US/additional.po +++ b/resources/lang/en_US/additional.po @@ -144,6 +144,9 @@ msgstr "Please enter your planned date of departure. " msgid "settings.profile.success" msgstr "Settings saved." +msgid "settings.sessions.delete_success" +msgstr "Session deleted successfully." + msgid "faq.delete.success" msgstr "FAQ entry successfully deleted." diff --git a/resources/lang/en_US/default.po b/resources/lang/en_US/default.po index 2594f45c..e77731c8 100644 --- a/resources/lang/en_US/default.po +++ b/resources/lang/en_US/default.po @@ -70,6 +70,9 @@ msgstr "Preview" msgid "form.delete" msgstr "Delete" +msgid "form.delete_all" +msgstr "Delete all" + msgid "form.updated" msgstr "Updated" @@ -299,6 +302,21 @@ msgstr "Password confirmation" msgid "settings.password.success" msgstr "Password was changed successfully." +msgid "settings.sessions" +msgstr "Sessions" + +msgid "settings.sessions.info" +msgstr "Here you can see and delete your browser sessions." + +msgid "settings.sessions.current" +msgstr "Current session" + +msgid "settings.sessions.id" +msgstr "Session ID" + +msgid "settings.sessions.last_activity" +msgstr "Last activity" + msgid "settings.theme" msgstr "Theme" diff --git a/resources/views/pages/settings/sessions.twig b/resources/views/pages/settings/sessions.twig new file mode 100644 index 00000000..2b0df9c9 --- /dev/null +++ b/resources/views/pages/settings/sessions.twig @@ -0,0 +1,61 @@ +{% extends 'pages/settings/settings.twig' %} +{% import 'macros/form.twig' as f %} +{% import 'macros/base.twig' as m %} + +{% block title %}{{ __('settings.sessions') }}{% endblock %} + +{% block row_content %} +
+
+ {{ m.info(__('settings.sessions.info')) }} + +
+ + + + + + + + + + + {% for session in sessions %} + + + + + + {% endfor %} + +
{{ __('settings.sessions.id') }}{{ __('settings.sessions.last_activity') }} + {% if sessions|length > 1 %} +
+ {{ csrf() }} + {{ f.hidden('id', 'all') }} + {{ f.submit( + __('form.delete_all'), + {'name': 'delete', 'btn_type': 'danger', 'size': 'sm', 'icon_left': 'trash'} + ) }} +
+ {% endif %} +
+
{{ session['id'] }}
+
{{ session.last_activity.format(__('Y-m-d H:i:s')) }} + {% if session.id != current_session %} +
+ {{ csrf() }} + {{ f.hidden('id', session.id) }} + {{ f.submit( + __('form.delete'), + {'name': 'delete', 'btn_type': 'danger', 'size': 'sm', 'icon_left': 'trash'} + ) }} +
+ {% else %} + {{ __('settings.sessions.current') }} + {% endif %} +
+
+
+
+{% endblock %} diff --git a/src/Controllers/SettingsController.php b/src/Controllers/SettingsController.php index fc0e0846..38dd5020 100644 --- a/src/Controllers/SettingsController.php +++ b/src/Controllers/SettingsController.php @@ -279,6 +279,38 @@ class SettingsController extends BaseController ); } + public function sessions(): Response + { + $sessions = $this->auth->user()->sessions->sortByDesc('last_activity'); + + return $this->response->withView( + 'pages/settings/sessions', + [ + 'settings_menu' => $this->settingsMenu(), + 'sessions' => $sessions, + 'current_session' => session()->getId(), + ], + ); + } + + public function sessionsDelete(Request $request): Response + { + $id = $request->postData('id'); + $query = $this->auth->user() + ->sessions() + ->getQuery() + ->where('id', '!=', session()->getId()); + + if ($id != 'all') { + $query = $query->where('id', $id); + } + + $query->delete(); + $this->addNotification('settings.sessions.delete_success'); + + return $this->redirect->to('/settings/sessions'); + } + public function settingsMenu(): array { $menu = [ @@ -298,6 +330,8 @@ class SettingsController extends BaseController $menu[url('/settings/certificates')] = 'settings.certificates'; } + $menu[url('/settings/sessions')] = 'settings.sessions'; + if (!empty(config('oauth'))) { $menu[url('/settings/oauth')] = ['title' => 'settings.oauth', 'hidden' => $this->checkOauthHidden()]; } diff --git a/tests/Unit/Controllers/SettingsControllerTest.php b/tests/Unit/Controllers/SettingsControllerTest.php index 77d74958..d3d3877c 100644 --- a/tests/Unit/Controllers/SettingsControllerTest.php +++ b/tests/Unit/Controllers/SettingsControllerTest.php @@ -11,6 +11,7 @@ use Engelsystem\Controllers\NotificationType; use Engelsystem\Controllers\SettingsController; use Engelsystem\Http\Exceptions\HttpNotFound; use Engelsystem\Http\Response; +use Engelsystem\Models\Session as SessionModel; use Engelsystem\Models\User\License; use Engelsystem\Models\User\Settings; use PHPUnit\Framework\MockObject\MockObject; @@ -32,6 +33,10 @@ class SettingsControllerTest extends ControllerTest protected SettingsController $controller; + protected SessionModel $currentSession; + protected SessionModel $secondSession; + protected SessionModel $otherSession; + protected function setUpProfileTest(): array { $body = [ @@ -574,7 +579,95 @@ class SettingsControllerTest extends ControllerTest $this->controller->oauth(); } + /** + * @covers \Engelsystem\Controllers\SettingsController::sessions + */ + public function testSessions(): void + { + $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + $this->response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function ($view, $data) { + $this->assertEquals('pages/settings/sessions', $view); + + $this->assertArrayHasKey('sessions', $data); + $this->assertCount(3, $data['sessions']); + + $this->assertArrayHasKey('current_session', $data); + $this->assertEquals($this->currentSession->id, $data['current_session']); + + return $this->response; + }); + + $this->controller->sessions(); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::sessionsDelete + */ + public function testSessionsDelete(): void + { + $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + $this->setExpects($this->response, 'redirectTo', ['http://localhost/settings/sessions'], $this->response); + + // Delete old user session + $this->request = $this->request->withParsedBody(['id' => $this->secondSession->id]); + $this->controller->sessionsDelete($this->request); + + $this->assertHasNotification('settings.sessions.delete_success'); + $this->assertCount(3, SessionModel::all()); + $this->assertNull(SessionModel::find($this->secondSession->id)); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::sessionsDelete + */ + public function testSessionsDeleteActiveSession(): void + { + $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + $this->setExpects($this->response, 'redirectTo', null, $this->response); + + // Delete active user session + $this->request = $this->request->withParsedBody(['id' => $this->currentSession->id]); + $this->controller->sessionsDelete($this->request); + + $this->assertCount(4, SessionModel::all()); // None got deleted + $this->assertNotNull(SessionModel::find($this->currentSession->id)); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::sessionsDelete + */ + public function testSessionsDeleteOtherUsersSession(): void + { + $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + $this->setExpects($this->response, 'redirectTo', null, $this->response); + + // Delete another users session + $this->request = $this->request->withParsedBody(['id' => $this->otherSession->id]); + $this->controller->sessionsDelete($this->request); + + $this->assertCount(4, SessionModel::all()); // None got deleted + $this->assertNotNull(SessionModel::find($this->otherSession->id)); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::sessionsDelete + */ + public function testSessionsDeleteAllSessions(): void + { + $this->setExpects($this->auth, 'user', null, $this->user, $this->once()); + $this->setExpects($this->response, 'redirectTo', null, $this->response); + + // Delete all other user sessions + $this->request = $this->request->withParsedBody(['id' => 'all']); + $this->controller->sessionsDelete($this->request); + + $this->assertCount(2, SessionModel::all()); // Two got deleted + $this->assertNotNull(SessionModel::find($this->currentSession->id)); + $this->assertNull(SessionModel::find($this->secondSession->id)); + } /** * @covers \Engelsystem\Controllers\SettingsController::__construct @@ -849,6 +942,13 @@ class SettingsControllerTest extends ControllerTest ->has(License::factory()) ->create(); + // Create 4 sessions, 3 for the active user + $this->otherSession = SessionModel::factory()->create()->first(); // Other users sessions + $sessions = SessionModel::factory(3)->create(['user_id' => $this->user->id]); + $this->currentSession = $sessions->first(); + $this->secondSession = $sessions->last(); + $this->session->setId($this->currentSession->id); + $this->controller = $this->app->make(SettingsController::class); $this->controller->setValidator(new Validator()); }