API keys can now have a description

This commit is contained in:
Bernd Bestel 2023-05-23 20:31:51 +02:00
parent bc5051351a
commit d0e0102752
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
8 changed files with 107 additions and 37 deletions

View File

@ -115,3 +115,4 @@
- The following entities are now also available via the endpoint `/objects/{entity}` (only listing, no edit) - The following entities are now also available via the endpoint `/objects/{entity}` (only listing, no edit)
- `quantity_unit_conversions_resolved` (returns all final/resolved conversion factors per product and any directly or indirectly related quantity units) - `quantity_unit_conversions_resolved` (returns all final/resolved conversion factors per product and any directly or indirectly related quantity units)
- The endpoint `/batteries` now also returns the corresponding battery object (as field/property `battery`) - The endpoint `/batteries` now also returns the corresponding battery object (as field/property `battery`)
- API keys can now have a description (to e.g. track where the corresponding key is used)

View File

@ -2,6 +2,7 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Services\ApiKeyService;
use Eluceo\iCal\Domain\Entity\Calendar; use Eluceo\iCal\Domain\Entity\Calendar;
use Eluceo\iCal\Domain\Entity\Event; use Eluceo\iCal\Domain\Entity\Event;
use Eluceo\iCal\Domain\Entity\TimeZone; use Eluceo\iCal\Domain\Entity\TimeZone;
@ -94,7 +95,7 @@ class CalendarApiController extends BaseApiController
try try
{ {
return $this->ApiResponse($response, [ return $this->ApiResponse($response, [
'url' => $this->AppContainer->get('UrlManager')->ConstructUrl('/api/calendar/ical?secret=' . $this->getApiKeyService()->GetOrCreateApiKey(\Grocy\Services\ApiKeyService::API_KEY_TYPE_SPECIAL_PURPOSE_CALENDAR_ICAL)) 'url' => $this->AppContainer->get('UrlManager')->ConstructUrl('/api/calendar/ical?secret=' . $this->getApiKeyService()->GetOrCreateApiKey(ApiKeyService::API_KEY_TYPE_SPECIAL_PURPOSE_CALENDAR_ICAL))
]); ]);
} }
catch (\Exception $ex) catch (\Exception $ex)

View File

@ -3,6 +3,7 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Controllers\Users\User; use Grocy\Controllers\Users\User;
use Grocy\Services\ApiKeyService;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
@ -10,22 +11,36 @@ class OpenApiController extends BaseApiController
{ {
public function ApiKeysList(Request $request, Response $response, array $args) public function ApiKeysList(Request $request, Response $response, array $args)
{ {
$selectedKeyId = -1;
if (isset($request->getQueryParams()['key']) && filter_var($request->getQueryParams()['key'], FILTER_VALIDATE_INT))
{
$selectedKeyId = $request->getQueryParams()['key'];
}
$apiKeys = $this->getDatabase()->api_keys(); $apiKeys = $this->getDatabase()->api_keys();
if (!User::hasPermissions(User::PERMISSION_ADMIN)) if (!User::hasPermissions(User::PERMISSION_ADMIN))
{ {
$apiKeys = $apiKeys->where('user_id', GROCY_USER_ID); $apiKeys = $apiKeys->where('user_id', GROCY_USER_ID);
} }
return $this->renderPage($response, 'manageapikeys', [ return $this->renderPage($response, 'manageapikeys', [
'apiKeys' => $apiKeys, 'apiKeys' => $apiKeys,
'users' => $this->getDatabase()->users() 'users' => $this->getDatabase()->users(),
'selectedKeyId' => $selectedKeyId
]); ]);
} }
public function CreateNewApiKey(Request $request, Response $response, array $args) public function CreateNewApiKey(Request $request, Response $response, array $args)
{ {
$newApiKey = $this->getApiKeyService()->CreateApiKey(); $description = null;
if (isset($request->getQueryParams()['description']))
{
$description = $request->getQueryParams()['description'];
}
$newApiKey = $this->getApiKeyService()->CreateApiKey(ApiKeyService::API_KEY_TYPE_DEFAULT, $description);
$newApiKeyId = $this->getApiKeyService()->GetApiKeyId($newApiKey); $newApiKeyId = $this->getApiKeyService()->GetApiKeyId($newApiKey);
return $response->withRedirect($this->AppContainer->get('UrlManager')->ConstructUrl("/manageapikeys?CreatedApiKeyId=$newApiKeyId")); return $response->withRedirect($this->AppContainer->get('UrlManager')->ConstructUrl("/manageapikeys?key=$newApiKeyId"));
} }
public function DocumentationSpec(Request $request, Response $response, array $args) public function DocumentationSpec(Request $request, Response $response, array $args)

2
migrations/0220.sql Normal file
View File

@ -0,0 +1,2 @@
ALTER TABLE api_keys
ADD description TEXT;

View File

@ -1,5 +1,5 @@
var apiKeysTable = $('#apikeys-table').DataTable({ var apiKeysTable = $('#apikeys-table').DataTable({
'order': [[4, 'desc']], 'order': [[6, 'desc']],
'columnDefs': [ 'columnDefs': [
{ 'orderable': false, 'targets': 0 }, { 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 } { 'searchable': false, "targets": 0 }
@ -8,12 +8,6 @@
$('#apikeys-table tbody').removeClass("d-none"); $('#apikeys-table tbody').removeClass("d-none");
apiKeysTable.columns.adjust().draw(); apiKeysTable.columns.adjust().draw();
var createdApiKeyId = GetUriParam('CreatedApiKeyId');
if (createdApiKeyId !== undefined)
{
animateCSS("#apiKeyRow_" + createdApiKeyId, "pulse");
}
$("#search").on("keyup", Delay(function() $("#search").on("keyup", Delay(function()
{ {
var value = $(this).val(); var value = $(this).val();
@ -33,8 +27,9 @@ $("#clear-filter-button").on("click", function()
$(document).on('click', '.apikey-delete-button', function(e) $(document).on('click', '.apikey-delete-button', function(e)
{ {
var objectName = $(e.currentTarget).attr('data-apikey-apikey'); var button = $(e.currentTarget);
var objectId = $(e.currentTarget).attr('data-apikey-id'); var objectName = button.attr('data-apikey-key');
var objectId = button.attr('data-apikey-id');
bootbox.confirm({ bootbox.confirm({
message: __t('Are you sure to delete API key "%s"?', objectName), message: __t('Are you sure to delete API key "%s"?', objectName),
@ -68,23 +63,36 @@ $(document).on('click', '.apikey-delete-button', function(e)
}); });
}); });
function QrCodeForApiKey(apiKeyType, apiKey) $(".apikey-show-qr-button").on("click", function()
{ {
var content = U('/api') + '|' + apiKey; var button = $(this);
if (apiKeyType === 'special-purpose-calendar-ical') var apiKey = button.data("apikey-key");
var apiKeyType = button.data("apikey-type");
var apiKeyDescription = button.data("apikey-description");
var content = U("/api") + "|" + apiKey;
if (apiKeyType === "special-purpose-calendar-ical")
{ {
content = U('/api/calendar/ical?secret=' + apiKey); content = U("/api/calendar/ical?secret=" + apiKey);
} }
return QrCodeImgHtml(content);
}
$('.apikey-show-qr-button').on('click', function()
{
var qrcodeHtml = QrCodeForApiKey($(this).data('apikey-type'), $(this).data('apikey-key'));
bootbox.alert({ bootbox.alert({
title: __t('API key'), message: "<h1>" + __t("API key") + "</h1><h2 class='text-muted'>" + apiKeyDescription + "</h2><p><hr>" + QrCodeImgHtml(content) + "</p>",
message: "<p class='text-center'>" + qrcodeHtml + "</p>",
closeButton: false closeButton: false
}); });
}) });
$("#add-api-key-button").on("click", function(e)
{
$("#add-api-key-modal").modal("show");
});
$("#add-api-key-modal").on("shown.bs.modal", function(e)
{
$("#description").focus();
});
$("#new-api-key-button").on("click", function(e)
{
window.location.href = U("/manageapikeys/new?description=" + encodeURIComponent($("#description").val()));
});

View File

@ -478,12 +478,12 @@ $("#add-recipe-modal").on("shown.bs.modal", function(e)
} }
Grocy.Components.RecipePicker.GetInputElement().focus(); Grocy.Components.RecipePicker.GetInputElement().focus();
}) });
$("#add-note-modal").on("shown.bs.modal", function(e) $("#add-note-modal").on("shown.bs.modal", function(e)
{ {
$("#note").focus(); $("#note").focus();
}) });
$("#add-product-modal").on("shown.bs.modal", function(e) $("#add-product-modal").on("shown.bs.modal", function(e)
{ {
@ -493,12 +493,12 @@ $("#add-product-modal").on("shown.bs.modal", function(e)
} }
Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductPicker.GetInputElement().focus();
}) });
$("#copy-day-modal").on("shown.bs.modal", function(e) $("#copy-day-modal").on("shown.bs.modal", function(e)
{ {
Grocy.Components.DateTimePicker2.GetInputElement().focus(); Grocy.Components.DateTimePicker2.GetInputElement().focus();
}) });
$(document).on("click", ".remove-recipe-button, .remove-note-button, .remove-product-button", function(e) $(document).on("click", ".remove-recipe-button, .remove-note-button, .remove-product-button", function(e)
{ {

View File

@ -8,15 +8,16 @@ class ApiKeyService extends BaseService
const API_KEY_TYPE_SPECIAL_PURPOSE_CALENDAR_ICAL = 'special-purpose-calendar-ical'; const API_KEY_TYPE_SPECIAL_PURPOSE_CALENDAR_ICAL = 'special-purpose-calendar-ical';
public function CreateApiKey($keyType = self::API_KEY_TYPE_DEFAULT) public function CreateApiKey(string $keyType = self::API_KEY_TYPE_DEFAULT, string $description = null)
{ {
$newApiKey = $this->GenerateApiKey(); $newApiKey = $this->GenerateApiKey();
$apiKeyRow = $this->getDatabase()->api_keys()->createRow([ $apiKeyRow = $this->getDatabase()->api_keys()->createRow([
'api_key' => $newApiKey, 'api_key' => $newApiKey,
'user_id' => GROCY_USER_ID, 'user_id' => GROCY_USER_ID,
'expires' => '2999-12-31 23:59:59', // Default is that API keys expire never 'expires' => '2999-12-31 23:59:59', // Default is that API keys never expire
'key_type' => $keyType 'key_type' => $keyType,
'description' => $description
]); ]);
$apiKeyRow->save(); $apiKeyRow->save();

View File

@ -4,6 +4,14 @@
@section('title', $__t('API keys')) @section('title', $__t('API keys'))
@push('pageStyles')
<style>
.modal-body {
text-align: center;
}
</style>
@endpush
@section('content') @section('content')
<div class="row"> <div class="row">
<div class="col"> <div class="col">
@ -25,8 +33,9 @@
</div> </div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100" <div class="related-links collapse d-md-flex order-2 width-xs-sm-100"
id="related-links"> id="related-links">
<a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right" <a id="add-api-key-button"
href="{{ $U('/manageapikeys/new') }}"> class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right"
href="#">
{{ $__t('Add') }} {{ $__t('Add') }}
</a> </a>
</div> </div>
@ -74,6 +83,7 @@
data-table-selector="#apikeys-table" data-table-selector="#apikeys-table"
href="#"><i class="fa-solid fa-eye"></i></a> href="#"><i class="fa-solid fa-eye"></i></a>
</th> </th>
<th>{{ $__t('Description') }}</th>
<th>{{ $__t('API key') }}</th> <th>{{ $__t('API key') }}</th>
<th class="allow-grouping">{{ $__t('User') }}</th> <th class="allow-grouping">{{ $__t('User') }}</th>
<th>{{ $__t('Expires') }}</th> <th>{{ $__t('Expires') }}</th>
@ -84,12 +94,12 @@
</thead> </thead>
<tbody class="d-none"> <tbody class="d-none">
@foreach($apiKeys as $apiKey) @foreach($apiKeys as $apiKey)
<tr id="apiKeyRow_{{ $apiKey->id }}"> <tr class="@if($apiKey->id == $selectedKeyId) table-primary @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-danger btn-sm apikey-delete-button" <a class="btn btn-danger btn-sm apikey-delete-button"
href="#" href="#"
data-apikey-id="{{ $apiKey->id }}" data-apikey-id="{{ $apiKey->id }}"
data-apikey-apikey="{{ $apiKey->api_key }}" data-apikey-key="{{ $apiKey->api_key }}"
data-toggle="tooltip" data-toggle="tooltip"
title="{{ $__t('Delete this item') }}"> title="{{ $__t('Delete this item') }}">
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-trash"></i>
@ -98,11 +108,15 @@
href="#" href="#"
data-apikey-key="{{ $apiKey->api_key }}" data-apikey-key="{{ $apiKey->api_key }}"
data-apikey-type="{{ $apiKey->key_type }}" data-apikey-type="{{ $apiKey->key_type }}"
data-apikey-description="{{ $apiKey->description }}"
data-toggle="tooltip" data-toggle="tooltip"
title="{{ $__t('Show a QR-Code for this API key') }}"> title="{{ $__t('Show a QR-Code for this API key') }}">
<i class="fa-solid fa-qrcode"></i> <i class="fa-solid fa-qrcode"></i>
</a> </a>
</td> </td>
<td>
{{ $apiKey->description }}
</td>
<td> <td>
{{ $apiKey->api_key }} {{ $apiKey->api_key }}
</td> </td>
@ -133,4 +147,32 @@
</table> </table>
</div> </div>
</div> </div>
<div class="modal fade"
id="add-api-key-modal"
tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title w-100">{{ $__t('Create new API key') }}</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label for="name">{{ $__t('Name') }}</label>
<input type="text"
class="form-control"
id="description"
name="description">
</div>
</div>
<div class="modal-footer">
<button type="button"
class="btn btn-secondary"
data-dismiss="modal">{{ $__t('Cancel') }}</button>
<button id="new-api-key-button"
class="btn btn-primary">{{ $__t('OK') }}</button>
</div>
</div>
</div>
</div>
@stop @stop