API: Add API settings page

This commit is contained in:
Igor Scheller 2023-11-16 18:20:50 +01:00 committed by Michael Weimann
parent fe836e281e
commit 4de882ef85
9 changed files with 243 additions and 0 deletions

View File

@ -43,6 +43,8 @@ $route->addGroup(
$route->get('/certificates', 'SettingsController@certificate'); $route->get('/certificates', 'SettingsController@certificate');
$route->post('/certificates/ifsg', 'SettingsController@saveIfsgCertificate'); $route->post('/certificates/ifsg', 'SettingsController@saveIfsgCertificate');
$route->post('/certificates/driving', 'SettingsController@saveDrivingLicense'); $route->post('/certificates/driving', 'SettingsController@saveDrivingLicense');
$route->get('/api', 'SettingsController@api');
$route->post('/api', 'SettingsController@apiKeyReset');
$route->get('/oauth', 'SettingsController@oauth'); $route->get('/oauth', 'SettingsController@oauth');
$route->get('/sessions', 'SettingsController@sessions'); $route->get('/sessions', 'SettingsController@sessions');
$route->post('/sessions', 'SettingsController@sessionsDelete'); $route->post('/sessions', 'SettingsController@sessionsDelete');

View File

@ -192,6 +192,9 @@ msgstr "Einstellungen gespeichert."
msgid "settings.sessions.delete_success" msgid "settings.sessions.delete_success"
msgstr "Sitzung erfolgreich gelöscht." msgstr "Sitzung erfolgreich gelöscht."
msgid "settings.api.key_reset_success"
msgstr "API Key erfolgreich zurückgesetzt."
msgid "faq.delete.success" msgid "faq.delete.success"
msgstr "FAQ Eintrag erfolgreich gelöscht." msgstr "FAQ Eintrag erfolgreich gelöscht."

View File

@ -1743,6 +1743,35 @@ msgstr "Hier kannst Du Deine Sprache ändern."
msgid "settings.language.success" msgid "settings.language.success"
msgstr "Sprache wurde erfolgreich geändert." 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" msgid "settings.oauth"
msgstr "Single Sign-On" msgstr "Single Sign-On"

View File

@ -191,6 +191,9 @@ msgstr "Settings saved."
msgid "settings.sessions.delete_success" msgid "settings.sessions.delete_success"
msgstr "Session deleted successfully." msgstr "Session deleted successfully."
msgid "settings.api.key_reset_success"
msgstr "API key successfully reset."
msgid "faq.delete.success" msgid "faq.delete.success"
msgstr "FAQ entry successfully deleted." msgstr "FAQ entry successfully deleted."

View File

@ -456,6 +456,35 @@ msgstr "Here you can change your language."
msgid "settings.language.success" msgid "settings.language.success"
msgstr "Language was changed successfully." 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" msgid "settings.oauth"
msgstr "Single Sign-On" msgstr "Single Sign-On"

View File

@ -253,6 +253,7 @@ Renders a button.
Must be a Bootstrap icon class without prefix, such as "info" or "check". 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_title] - Optional value for the confirmation title.
@param {string} [opt.confirm_text] - Optional value for the confirmation text. @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) %} {% macro button(label, opt) %}
{%- set icon_left = opt.icon_left is defined ? '<span class="bi bi-' ~ opt.icon_left ~ '"></span>' : '' %} {%- set icon_left = opt.icon_left is defined ? '<span class="bi bi-' ~ opt.icon_left ~ '"></span>' : '' %}
@ -269,6 +270,7 @@ Renders a button.
{%- if opt.confirm_button_text is defined %} {%- if opt.confirm_button_text is defined %}
data-confirm_button_text="{{ icon_left ~ ' ' ~ opt.confirm_button_text ~ ' ' ~ icon_right }}" data-confirm_button_text="{{ icon_left ~ ' ' ~ opt.confirm_button_text ~ ' ' ~ icon_right }}"
{%- endif -%} {%- endif -%}
{%- for key, value in opt.attr|default({}) %} {{ key }}="{{ value }}"{% endfor -%}
> >
{{ icon_left|raw }} {{ icon_left|raw }}
{{ label }} {{ label }}

View File

@ -0,0 +1,95 @@
{% extends 'pages/settings/settings.twig' %}
{% import 'macros/form.twig' as f %}
{% block title %}{{ __('settings.api') }}{% endblock %}
{% block row_content %}
<div content="row">
<div class="col-md-12">
{{ 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 %}
<form method="post" class="d-inline">
{{ csrf() }}
{{ f.submit(
__('settings.api.key_reset'),
{ 'size': 'sm', 'icon_left': 'arrow-repeat', 'confirm_text': __('settings.api.key_reset_confirm') }
) }}
</form>
</div>
<div class="col-md-12 pt-2" id="exports_hide">
<p id="key_hide" class="collapse" data-bs-parent="#exports_hide">
<code>{{ user.api_key }}</code>
</p>
{% if has_permission_to('shifts_json_export') %}
<p id="shifts_json_hide" class="collapse" data-bs-parent="#exports_hide">
<code>{{ url('/shifts-json-export', {'key': user.api_key}) }}</code>
</p>
{% endif %}
{% if has_permission_to('ical') %}
<p id="ical_hide" class="collapse" data-bs-parent="#exports_hide">
<code>{{ url('/ical', {'key': user.api_key}) }}</code>
</p>
{% endif %}
{% if has_permission_to('atom') %}
<p id="news_hide" class="collapse" data-bs-parent="#exports_hide">
<code>{{ url('/atom', {'key': user.api_key}) }}</code>
<code>{{ url('/atom', {'meetings': 1, 'key': user.api_key}) }}</code>
<code>{{ url('/rss', {'key': user.api_key}) }}</code>
<code>{{ url('/rss', {'meetings': 1, 'key': user.api_key}) }}</code>
</p>
{% endif %}
</div>
</div>
<div class="row">
<div class="col-md-12">
{{ __('settings.api.about', [url('/api/v0-beta'), url('/api/v0-beta/openapi')])|markdown|nl2br }}
</div>
</div>
{% endblock %}

View File

@ -22,6 +22,8 @@ class SettingsController extends BaseController
/** @var string[] */ /** @var string[] */
protected array $permissions = [ protected array $permissions = [
'user_settings', 'user_settings',
'api' => 'api',
'apiKeyReset' => 'api',
]; ];
public function __construct( public function __construct(
@ -305,6 +307,24 @@ class SettingsController extends BaseController
return $this->redirect->to('/settings/certificates'); 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 public function oauth(): Response
{ {
$providers = $this->config->get('oauth'); $providers = $this->config->get('oauth');
@ -382,6 +402,10 @@ class SettingsController extends BaseController
$menu[url('/settings/oauth')] = ['title' => 'settings.oauth', 'hidden' => $this->checkOauthHidden()]; $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; return $menu;
} }

View File

@ -10,6 +10,7 @@ use Engelsystem\Config\GoodieType;
use Engelsystem\Controllers\NotificationType; use Engelsystem\Controllers\NotificationType;
use Engelsystem\Controllers\SettingsController; use Engelsystem\Controllers\SettingsController;
use Engelsystem\Http\Exceptions\HttpNotFound; use Engelsystem\Http\Exceptions\HttpNotFound;
use Engelsystem\Http\Redirector;
use Engelsystem\Http\Response; use Engelsystem\Http\Response;
use Engelsystem\Models\AngelType; use Engelsystem\Models\AngelType;
use Engelsystem\Models\Session as SessionModel; use Engelsystem\Models\Session as SessionModel;
@ -886,6 +887,41 @@ class SettingsControllerTest extends ControllerTest
$this->controller->saveDrivingLicense($this->request); $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 * @covers \Engelsystem\Controllers\SettingsController::settingsMenu
@ -982,6 +1018,26 @@ class SettingsControllerTest extends ControllerTest
$this->assertArrayNotHasKey('http://localhost/settings/certificates', $menu); $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 * Setup environment
*/ */