diff --git a/config/routes.php b/config/routes.php index fe313bee..25d3bc69 100644 --- a/config/routes.php +++ b/config/routes.php @@ -43,6 +43,8 @@ $route->addGroup( $route->get('/certificates', 'SettingsController@certificate'); $route->post('/certificates/ifsg', 'SettingsController@saveIfsgCertificate'); $route->post('/certificates/driving', 'SettingsController@saveDrivingLicense'); + $route->get('/api', 'SettingsController@api'); + $route->post('/api', 'SettingsController@apiKeyReset'); $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 870ccf6b..1975b54c 100644 --- a/resources/lang/de_DE/additional.po +++ b/resources/lang/de_DE/additional.po @@ -192,6 +192,9 @@ msgstr "Einstellungen gespeichert." msgid "settings.sessions.delete_success" msgstr "Sitzung erfolgreich gelöscht." +msgid "settings.api.key_reset_success" +msgstr "API Key erfolgreich zurückgesetzt." + 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 c9ca1961..104844cb 100644 --- a/resources/lang/de_DE/default.po +++ b/resources/lang/de_DE/default.po @@ -1743,6 +1743,35 @@ msgstr "Hier kannst Du Deine Sprache ändern." msgid "settings.language.success" msgstr "Sprache wurde erfolgreich geändert." +msgid "settings.api" +msgstr "API" + +msgid "settings.api.about" +msgstr "" +"Die API erlaubt es dir, über externe Programme, mit dem Engelsystem zu interagieren. " +"Sie ist noch nicht vollständig, wir arbeiten aber daran sie zu erweitern.\n" +"Der API Einstiegspunkt befindet sich unter `%s` und ist in der [OpenAPI Spezifikation](%s) beschrieben.\n" +"Teile deinen persönlichen API Key mit niemandem, er erlaubt es deine persönlichen Daten einzusehen " +"und Änderungen in deinem Namen durch zu führen!" + +msgid "settings.api.shifts_json_show" +msgstr "JSON Schichten Export anzeigen" + +msgid "settings.api.ical_show" +msgstr "iCal export anzeigen" + +msgid "settings.api.news_show" +msgstr "News feeds anzeigen" + +msgid "settings.api.key_show" +msgstr "API Key anzeigen" + +msgid "settings.api.key_reset" +msgstr "API Key zurücksetzen" + +msgid "settings.api.key_reset_confirm" +msgstr "Wenn du den API Key zurücksetzt, musst ihn in allen deinen Anwendungen aktualisieren." + msgid "settings.oauth" msgstr "Single Sign-On" diff --git a/resources/lang/en_US/additional.po b/resources/lang/en_US/additional.po index 7bc4a8e2..3e7d1dc1 100644 --- a/resources/lang/en_US/additional.po +++ b/resources/lang/en_US/additional.po @@ -191,6 +191,9 @@ msgstr "Settings saved." msgid "settings.sessions.delete_success" msgstr "Session deleted successfully." +msgid "settings.api.key_reset_success" +msgstr "API key successfully reset." + 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 f7891cb1..cec17e52 100644 --- a/resources/lang/en_US/default.po +++ b/resources/lang/en_US/default.po @@ -456,6 +456,35 @@ msgstr "Here you can change your language." msgid "settings.language.success" msgstr "Language was changed successfully." +msgid "settings.api" +msgstr "API" + +msgid "settings.api.about" +msgstr "" +"The API allows you to interact with the Engelsystem by using external programs. " +"It's not complete but we are working on extending it.\n" +"The API endpoint is located at `%s` and described in the [OpenAPI specification](%s).\n" +"Don't share your personal API key with anyone as it can be used to view your personal data " +"and do changes your behalf!" + +msgid "settings.api.shifts_json_show" +msgstr "Show JSON shifts export" + +msgid "settings.api.ical_show" +msgstr "Show iCal export" + +msgid "settings.api.news_show" +msgstr "Show news feeds" + +msgid "settings.api.key_show" +msgstr "Show API key" + +msgid "settings.api.key_reset" +msgstr "Reset API key" + +msgid "settings.api.key_reset_confirm" +msgstr "If you reset the API key you have to update it in all your applications." + msgid "settings.oauth" msgstr "Single Sign-On" diff --git a/resources/views/macros/form.twig b/resources/views/macros/form.twig index 8d3dc8d9..c55b1c57 100644 --- a/resources/views/macros/form.twig +++ b/resources/views/macros/form.twig @@ -253,6 +253,7 @@ Renders a button. Must be a Bootstrap icon class without prefix, such as "info" or "check". @param {string} [opt.confirm_title] - Optional value for the confirmation title. @param {string} [opt.confirm_text] - Optional value for the confirmation text. +@param {dictionary} [opt.attr] - Optional value for additional attributes like data fields. #} {% macro button(label, opt) %} {%- set icon_left = opt.icon_left is defined ? '' : '' %} @@ -269,6 +270,7 @@ Renders a button. {%- if opt.confirm_button_text is defined %} data-confirm_button_text="{{ icon_left ~ ' ' ~ opt.confirm_button_text ~ ' ' ~ icon_right }}" {%- endif -%} + {%- for key, value in opt.attr|default({}) %} {{ key }}="{{ value }}"{% endfor -%} > {{ icon_left|raw }} {{ label }} diff --git a/resources/views/pages/settings/api.twig b/resources/views/pages/settings/api.twig new file mode 100644 index 00000000..0dd33487 --- /dev/null +++ b/resources/views/pages/settings/api.twig @@ -0,0 +1,95 @@ +{% extends 'pages/settings/settings.twig' %} +{% import 'macros/form.twig' as f %} + +{% block title %}{{ __('settings.api') }}{% endblock %} + +{% block row_content %} + +
+
+ + {{ f.button( + __('settings.api.key_show'), + {'size': 'sm', 'icon_left': 'key', 'attr': { + 'data-bs-toggle': 'collapse', 'data-bs-target': '#key_hide', + 'aria-expanded': 'true', 'aria-controls': 'key_hide' + }} + ) }} + + {% if has_permission_to('shifts_json_export') %} + {{ f.button( + __('settings.api.shifts_json_show'), + {'size': 'sm', 'icon_left': 'braces', 'attr': { + 'data-bs-toggle': 'collapse', 'data-bs-target': '#shifts_json_hide', + 'aria-expanded': 'true', 'aria-controls': 'shifts_json_hide' + }} + ) }} + {% endif %} + + {% if has_permission_to('ical') %} + {{ f.button( + __('settings.api.ical_show'), + {'size': 'sm', 'icon_left': 'calendar-week', 'attr': { + 'data-bs-toggle': 'collapse', 'data-bs-target': '#ical_hide', + 'aria-expanded': 'true', 'aria-controls': 'ical_hide' + }} + ) }} + {% endif %} + + {% if has_permission_to('atom') %} + {{ f.button( + __('settings.api.news_show'), + {'size': 'sm', 'icon_left': 'calendar-week', 'attr': { + 'data-bs-toggle': 'collapse', 'data-bs-target': '#news_hide', + 'aria-expanded': 'true', 'aria-controls': 'news_hide' + }} + ) }} + {% endif %} + +
+ {{ csrf() }} + {{ f.submit( + __('settings.api.key_reset'), + { 'size': 'sm', 'icon_left': 'arrow-repeat', 'confirm_text': __('settings.api.key_reset_confirm') } + ) }} +
+ +
+ +
+ +

+ {{ user.api_key }} +

+ + {% if has_permission_to('shifts_json_export') %} +

+ {{ url('/shifts-json-export', {'key': user.api_key}) }} +

+ {% endif %} + + {% if has_permission_to('ical') %} +

+ {{ url('/ical', {'key': user.api_key}) }} +

+ {% endif %} + + {% if has_permission_to('atom') %} +

+ {{ url('/atom', {'key': user.api_key}) }} + {{ url('/atom', {'meetings': 1, 'key': user.api_key}) }} + {{ url('/rss', {'key': user.api_key}) }} + {{ url('/rss', {'meetings': 1, 'key': user.api_key}) }} +

+ {% endif %} + +
+
+ +
+
+ {{ __('settings.api.about', [url('/api/v0-beta'), url('/api/v0-beta/openapi')])|markdown|nl2br }} +
+
+ +{% endblock %} diff --git a/src/Controllers/SettingsController.php b/src/Controllers/SettingsController.php index 8a28e1b5..a00620eb 100644 --- a/src/Controllers/SettingsController.php +++ b/src/Controllers/SettingsController.php @@ -22,6 +22,8 @@ class SettingsController extends BaseController /** @var string[] */ protected array $permissions = [ 'user_settings', + 'api' => 'api', + 'apiKeyReset' => 'api', ]; public function __construct( @@ -305,6 +307,24 @@ class SettingsController extends BaseController return $this->redirect->to('/settings/certificates'); } + public function api(): Response + { + return $this->response->withView( + 'pages/settings/api', + [ + 'settings_menu' => $this->settingsMenu(), + ], + ); + } + + public function apiKeyReset(): Response + { + $this->auth->resetApiKey($this->auth->user()); + + $this->addNotification('settings.api.key_reset_success'); + return $this->redirect->back(); + } + public function oauth(): Response { $providers = $this->config->get('oauth'); @@ -382,6 +402,10 @@ class SettingsController extends BaseController $menu[url('/settings/oauth')] = ['title' => 'settings.oauth', 'hidden' => $this->checkOauthHidden()]; } + if ($this->auth->can('api')) { + $menu[url('/settings/api')] = ['title' => 'settings.api', 'icon' => 'braces']; + } + return $menu; } diff --git a/tests/Unit/Controllers/SettingsControllerTest.php b/tests/Unit/Controllers/SettingsControllerTest.php index 3729ed18..af961809 100644 --- a/tests/Unit/Controllers/SettingsControllerTest.php +++ b/tests/Unit/Controllers/SettingsControllerTest.php @@ -10,6 +10,7 @@ use Engelsystem\Config\GoodieType; use Engelsystem\Controllers\NotificationType; use Engelsystem\Controllers\SettingsController; use Engelsystem\Http\Exceptions\HttpNotFound; +use Engelsystem\Http\Redirector; use Engelsystem\Http\Response; use Engelsystem\Models\AngelType; use Engelsystem\Models\Session as SessionModel; @@ -886,6 +887,41 @@ class SettingsControllerTest extends ControllerTest $this->controller->saveDrivingLicense($this->request); } + /** + * @covers \Engelsystem\Controllers\SettingsController::api + * @covers \Engelsystem\Controllers\SettingsController::settingsMenu + */ + public function testApi(): void + { + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); + + /** @var Response|MockObject $response */ + $this->response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function ($view, $data) { + $this->assertEquals('pages/settings/api', $view); + $this->assertArrayHasKey('settings_menu', $data); + return $this->response; + }); + + $this->controller = $this->app->make(SettingsController::class); + $this->controller->api(); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::apiKeyReset + */ + public function testApiKeyReset(): void + { + $redirector = $this->createMock(Redirector::class); + $this->app->instance(Redirector::class, $redirector); + $this->setExpects($this->auth, 'user', null, $this->user, $this->atLeastOnce()); + $this->setExpects($this->auth, 'resetApiKey', [$this->user], null, $this->atLeastOnce()); + $this->setExpects($redirector, 'back', null, $this->response); + + $this->controller = $this->app->make(SettingsController::class); + $this->controller->apiKeyReset(); + } /** * @covers \Engelsystem\Controllers\SettingsController::settingsMenu @@ -982,6 +1018,26 @@ class SettingsControllerTest extends ControllerTest $this->assertArrayNotHasKey('http://localhost/settings/certificates', $menu); } + /** + * @covers \Engelsystem\Controllers\SettingsController::settingsMenu + */ + public function testSettingsMenuApi(): void + { + $this->setExpects($this->auth, 'can', ['api'], true, $this->atLeastOnce()); + + $menu = $this->controller->settingsMenu(); + $this->assertArrayHasKey('http://localhost/settings/profile', $menu); + } + + /** + * @covers \Engelsystem\Controllers\SettingsController::settingsMenu + */ + public function testSettingsMenuApiNotAvailable(): void + { + $menu = $this->controller->settingsMenu(); + $this->assertArrayNotHasKey('http://localhost/settings/api', $menu); + } + /** * Setup environment */