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)
- `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`)
- 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;
use Grocy\Services\ApiKeyService;
use Eluceo\iCal\Domain\Entity\Calendar;
use Eluceo\iCal\Domain\Entity\Event;
use Eluceo\iCal\Domain\Entity\TimeZone;
@ -94,7 +95,7 @@ class CalendarApiController extends BaseApiController
try
{
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)

View File

@ -3,6 +3,7 @@
namespace Grocy\Controllers;
use Grocy\Controllers\Users\User;
use Grocy\Services\ApiKeyService;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
@ -10,22 +11,36 @@ class OpenApiController extends BaseApiController
{
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();
if (!User::hasPermissions(User::PERMISSION_ADMIN))
{
$apiKeys = $apiKeys->where('user_id', GROCY_USER_ID);
}
return $this->renderPage($response, 'manageapikeys', [
'apiKeys' => $apiKeys,
'users' => $this->getDatabase()->users()
'users' => $this->getDatabase()->users(),
'selectedKeyId' => $selectedKeyId
]);
}
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);
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)

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({
'order': [[4, 'desc']],
'order': [[6, 'desc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
@ -8,12 +8,6 @@
$('#apikeys-table tbody').removeClass("d-none");
apiKeysTable.columns.adjust().draw();
var createdApiKeyId = GetUriParam('CreatedApiKeyId');
if (createdApiKeyId !== undefined)
{
animateCSS("#apiKeyRow_" + createdApiKeyId, "pulse");
}
$("#search").on("keyup", Delay(function()
{
var value = $(this).val();
@ -33,8 +27,9 @@ $("#clear-filter-button").on("click", function()
$(document).on('click', '.apikey-delete-button', function(e)
{
var objectName = $(e.currentTarget).attr('data-apikey-apikey');
var objectId = $(e.currentTarget).attr('data-apikey-id');
var button = $(e.currentTarget);
var objectName = button.attr('data-apikey-key');
var objectId = button.attr('data-apikey-id');
bootbox.confirm({
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;
if (apiKeyType === 'special-purpose-calendar-ical')
var button = $(this);
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({
title: __t('API key'),
message: "<p class='text-center'>" + qrcodeHtml + "</p>",
message: "<h1>" + __t("API key") + "</h1><h2 class='text-muted'>" + apiKeyDescription + "</h2><p><hr>" + QrCodeImgHtml(content) + "</p>",
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();
})
});
$("#add-note-modal").on("shown.bs.modal", function(e)
{
$("#note").focus();
})
});
$("#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();
})
});
$("#copy-day-modal").on("shown.bs.modal", function(e)
{
Grocy.Components.DateTimePicker2.GetInputElement().focus();
})
});
$(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';
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();
$apiKeyRow = $this->getDatabase()->api_keys()->createRow([
'api_key' => $newApiKey,
'user_id' => GROCY_USER_ID,
'expires' => '2999-12-31 23:59:59', // Default is that API keys expire never
'key_type' => $keyType
'expires' => '2999-12-31 23:59:59', // Default is that API keys never expire
'key_type' => $keyType,
'description' => $description
]);
$apiKeyRow->save();

View File

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