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')) }}
+
+
+
+
+
+ {{ __('settings.sessions.id') }} |
+ {{ __('settings.sessions.last_activity') }} |
+
+ {% if sessions|length > 1 %}
+
+ {% endif %}
+ |
+
+
+
+
+ {% for session in sessions %}
+
+
+ {{ session['id'] }}
+ |
+ {{ session.last_activity.format(__('Y-m-d H:i:s')) }} |
+
+ {% if session.id != current_session %}
+
+ {% else %}
+ {{ __('settings.sessions.current') }}
+ {% endif %}
+ |
+
+ {% endfor %}
+
+
+
+
+
+{% 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());
}