Skip to content

Commit 0a34de8

Browse files
authored
Merge pull request #246 from os2display/release/2.5.1
Release 2.5.1
2 parents cd734a2 + 932b7cb commit 0a34de8

File tree

7 files changed

+124
-31
lines changed

7 files changed

+124
-31
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
## [2.5.1] - 2025-06-23
8+
9+
- [#245](https://github.com/os2display/display-api-service/pull/245)
10+
- Added resource endpoint for limiting access to instant book interactive slide.
11+
- [#243](https://github.com/os2display/display-api-service/pull/243)
12+
- Changed resource name in calendar api feed type resource selector.
13+
- [#242](https://github.com/os2display/display-api-service/pull/242)
14+
- Changed calendar api feed type to use display name from resources feed instead of events feed.
15+
716
## [2.5.0] - 2025-05-09
817

918
- [#240](https://github.com/os2display/display-api-service/pull/240)

docs/feed/calender-api-feed.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,14 @@ By default, the three endpoints should return data as follows:
3838
{
3939
"id": "Resource Id 1",
4040
"locationId": "Location Id 1",
41+
"name": "Resource-1",
4142
"displayName": "Resource 1",
4243
"includedInEvents": true
4344
},
4445
{
4546
"id": "Resource Id 2",
4647
"locationId": "Location Id 1",
48+
"name": "Resource-2",
4749
"displayName": "Resource 2",
4850
"includedInEvents": false
4951
}
@@ -52,7 +54,8 @@ By default, the three endpoints should return data as follows:
5254

5355
* The `id` (Mapping key: RESOURCE_ID) should be unique for the resource.
5456
* The `locationId` (Mapping key: RESOURCE_LOCATION_ID) is the id of the location the resource belongs to.
55-
* The `displayName` (Mapping key: RESOURCE_DISPLAY_NAME) is the name the resource is presented by in templates and admin.
57+
* The `name` (Mapping key: RESOURCE_NAME) is the name the resource is presented by in the admin selector.
58+
* The `displayName` (Mapping key: RESOURCE_DISPLAY_NAME) is the name the resource is presented by in templates.
5659
* The `includedInEvents` (Mapping key: RESOURCE_INCLUDED_IN_EVENTS) determines if the resource is included in the events
5760
endpoint.
5861
This property can be excluded in the data. If this is the case, it defaults to `true`.
@@ -102,6 +105,7 @@ CALENDAR_API_FEED_SOURCE_CUSTOM_MAPPINGS='{
102105
"LOCATION_DISPLAY_NAME": "Example2",
103106
"RESOURCE_ID": "Example3",
104107
"RESOURCE_LOCATION_ID": "Example4",
108+
"RESOURCE_NAME": "Example12",
105109
"RESOURCE_DISPLAY_NAME": "Example5",
106110
"RESOURCE_INCLUDED_IN_EVENTS": "Example6",
107111
"EVENT_TITLE": "Example7",

infrastructure/itkdev/display-api-service/etc/confd/templates/env.local.tmpl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,5 @@ EVENTDATABASE_API_V2_CACHE_EXPIRE_SECONDS={{ getenv "APP_EVENTDATABASE_API_V2_CA
6767

6868
TRACK_SCREEN_INFO={{ getenv "APP_TRACK_SCREEN_INFO" "false" }}
6969
TRACK_SCREEN_INFO_UPDATE_INTERVAL_SECONDS={{ getenv "APP_TRACK_SCREEN_INFO_UPDATE_INTERVAL_SECONDS" "300" }}
70+
71+
APP_KEY_VAULT_JSON={{ getenv "APP_KEY_VAULT_JSON" "{}" }}

infrastructure/os2display/display-api-service/etc/confd/templates/env.local.tmpl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,5 @@ EVENTDATABASE_API_V2_CACHE_EXPIRE_SECONDS={{ getenv "APP_EVENTDATABASE_API_V2_CA
6767

6868
TRACK_SCREEN_INFO={{ getenv "APP_TRACK_SCREEN_INFO" "false" }}
6969
TRACK_SCREEN_INFO_UPDATE_INTERVAL_SECONDS={{ getenv "APP_TRACK_SCREEN_INFO_UPDATE_INTERVAL_SECONDS" "300" }}
70+
71+
APP_KEY_VAULT_JSON={{ getenv "APP_KEY_VAULT_JSON" "{}" }}

src/Feed/CalendarApiFeedType.php

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -72,24 +72,35 @@ public function getData(Feed $feed): array
7272
return [];
7373
}
7474

75-
$resources = $configuration['resources'];
75+
$requestedResources = $configuration['resources'];
76+
77+
$allResources = $this->loadResources();
7678

7779
$events = [];
7880

79-
foreach ($resources as $resource) {
80-
$events += $this->getResourceEvents($resource);
81+
foreach ($requestedResources as $requestedResource) {
82+
$events = array_merge($events, $this->getResourceEvents($requestedResource));
8183
}
8284

8385
$modifiedResults = static::applyModifiersToEvents($events, $this->eventModifiers, $enabledModifiers);
8486

85-
$resultsAsArray = array_map(fn (CalendarEvent $event) => [
86-
'id' => Ulid::generate(),
87-
'title' => $event->title,
88-
'startTime' => $event->startTimeTimestamp,
89-
'endTime' => $event->endTimeTimestamp,
90-
'resourceTitle' => $event->resourceDisplayName,
91-
'resourceId' => $event->resourceId,
92-
], $modifiedResults);
87+
$resultsAsArray = array_map(function (CalendarEvent $event) use ($allResources) {
88+
$resourceDisplayName = $event->resourceDisplayName;
89+
90+
// Override resource title with resource display name from resources list.
91+
if (isset($allResources[$event->resourceId])) {
92+
$resourceDisplayName = $allResources[$event->resourceId]->displayName;
93+
}
94+
95+
return [
96+
'id' => Ulid::generate(),
97+
'title' => $event->title,
98+
'startTime' => $event->startTimeTimestamp,
99+
'endTime' => $event->endTimeTimestamp,
100+
'resourceTitle' => $resourceDisplayName,
101+
'resourceId' => $event->resourceId,
102+
];
103+
}, $modifiedResults);
93104

94105
// Sort bookings by start time.
95106
usort($resultsAsArray, fn (array $a, array $b) => $a['startTime'] > $b['startTime'] ? 1 : -1);
@@ -217,7 +228,7 @@ public function getConfigOptions(Request $request, FeedSource $feedSource, strin
217228

218229
$resourceOptions = array_map(fn (Resource $resource) => [
219230
'id' => Ulid::generate(),
220-
'title' => $resource->displayName,
231+
'title' => $resource->name.' ('.$resource->displayName.')',
221232
'value' => $resource->id,
222233
], $resources);
223234

@@ -357,13 +368,16 @@ private function loadResources(): array
357368

358369
// Only include resources that are included in events endpoint.
359370
if ($includeValue) {
371+
$id = $resourceEntry[$this->getMapping('resourceId')];
372+
360373
$resource = new Resource(
361-
$resourceEntry[$this->getMapping('resourceId')],
374+
$id,
375+
$resourceEntry[$this->getMapping('resourceName')],
362376
$resourceEntry[$this->getMapping('resourceLocationId')],
363377
$resourceEntry[$this->getMapping('resourceDisplayName')],
364378
);
365379

366-
$resources[] = $resource;
380+
$resources[$id] = $resource;
367381
}
368382
}
369383

@@ -472,6 +486,7 @@ private function createMappings(array $customMappings): array
472486
'locationDisplayName' => $customMappings['LOCATION_DISPLAY_NAME'] ?? 'displayName',
473487
'resourceId' => $customMappings['RESOURCE_ID'] ?? 'id',
474488
'resourceLocationId' => $customMappings['RESOURCE_LOCATION_ID'] ?? 'locationId',
489+
'resourceName' => $customMappings['RESOURCE_NAME'] ?? 'name',
475490
'resourceDisplayName' => $customMappings['RESOURCE_DISPLAY_NAME'] ?? 'displayName',
476491
'resourceIncludedInEvents' => $customMappings['RESOURCE_INCLUDED_IN_EVENTS'] ?? 'includedInEvents',
477492
'eventTitle' => $customMappings['EVENT_TITLE'] ?? 'title',

src/Feed/OutputModel/Calendar/Resource.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class Resource
88
{
99
public function __construct(
1010
public string $id,
11+
public string $name,
1112
public string $locationId,
1213
public string $displayName,
1314
) {}

src/InteractiveSlide/InstantBook.php

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
use App\Entity\Tenant\InteractiveSlide;
1010
use App\Entity\Tenant\Slide;
1111
use App\Entity\User;
12-
use App\Exceptions\InteractiveSlideException;
1312
use App\Service\InteractiveSlideService;
1413
use App\Service\KeyVaultService;
1514
use Psr\Cache\CacheItemInterface;
1615
use Psr\Cache\InvalidArgumentException;
1716
use Symfony\Bundle\SecurityBundle\Security;
17+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
1818
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
1919
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
2020
use Symfony\Component\Security\Core\User\UserInterface;
@@ -36,6 +36,7 @@ class InstantBook implements InteractiveSlideInterface
3636
private const string SCOPE = 'https://graph.microsoft.com/.default';
3737
private const string GRANT_TYPE = 'password';
3838
private const string CACHE_PREFIX = 'MS-INSTANT-BOOK';
39+
private const string CACHE_ALLOWED_RESOURCES_PREFIX = 'INSTANT-BOOK-ALLOWED-RESOURCES-';
3940
private const string CACHE_KEY_TOKEN_PREFIX = self::CACHE_PREFIX.'-TOKEN-';
4041
private const string CACHE_KEY_OPTIONS_PREFIX = self::CACHE_PREFIX.'-OPTIONS-';
4142
private const string CACHE_PREFIX_SPAM_PROTECT_PREFIX = self::CACHE_PREFIX.'-SPAM-PROTECT-';
@@ -58,23 +59,29 @@ public function __construct(
5859

5960
public function getConfigOptions(): array
6061
{
62+
// All secrets are retrieved from the KeyVault. Therefore, the input for the different configurations are the
63+
// keys into the KeyVault where the values can be retrieved.
6164
return [
6265
'tenantId' => [
6366
'required' => true,
64-
'description' => 'The key in the KeyVault for the tenant id of the App',
67+
'description' => 'The key in the KeyVault for the tenant id of the Microsoft Graph App',
6568
],
6669
'clientId' => [
6770
'required' => true,
68-
'description' => 'The key in the KeyVault for the client id of the App',
71+
'description' => 'The key in the KeyVault for the client id of the Microsoft Graph App',
6972
],
7073
'username' => [
7174
'required' => true,
72-
'description' => 'The key in the KeyVault for the Microsoft Graph username that should perform the action.',
75+
'description' => 'The key in the KeyVault for the username that should perform the action.',
7376
],
7477
'password' => [
7578
'required' => true,
7679
'description' => 'The key in the KeyVault for the password of the user.',
7780
],
81+
'resourceEndpoint' => [
82+
'required' => false,
83+
'description' => 'The key in the KeyVault for the resources endpoint. This should supply a json list of resources that can be booked. The resources should have ResourceMail and allowInstantBooking ("True"/"False") properties set.',
84+
],
7885
];
7986
}
8087

@@ -83,7 +90,7 @@ public function performAction(UserInterface $user, Slide $slide, InteractionSlid
8390
return match ($interactionRequest->action) {
8491
self::ACTION_GET_QUICK_BOOK_OPTIONS => $this->getQuickBookOptions($slide, $interactionRequest),
8592
self::ACTION_QUICK_BOOK => $this->quickBook($slide, $interactionRequest),
86-
default => throw new InteractiveSlideException('Action not allowed'),
93+
default => throw new BadRequestHttpException('Action not allowed'),
8794
};
8895
}
8996

@@ -98,7 +105,7 @@ private function authenticate(array $configuration): array
98105
$password = $this->keyValueService->getValue($configuration['password']);
99106

100107
if (4 !== count(array_filter([$tenantId, $clientId, $username, $password]))) {
101-
throw new \Exception('tenantId, clientId, username, password must all be set.');
108+
throw new BadRequestHttpException('tenantId, clientId, username, password must all be set.');
102109
}
103110

104111
$url = self::LOGIN_ENDPOINT.$tenantId.self::OAUTH_PATH;
@@ -124,7 +131,7 @@ private function getToken(Tenant $tenant, InteractiveSlide $interactive): string
124131
$configuration = $interactive->getConfiguration();
125132

126133
if (null === $configuration) {
127-
throw new \Exception('InteractiveNoConfiguration');
134+
throw new BadRequestHttpException('InteractiveSlide has no configuration');
128135
}
129136

130137
return $this->interactiveSlideCache->get(
@@ -161,13 +168,16 @@ function (CacheItemInterface $item) use ($slide, $resource, $interactionRequest)
161168
$interactive = $this->interactiveService->getInteractiveSlide($tenant, $interactionRequest->implementationClass);
162169

163170
if (null === $interactive) {
164-
throw new \Exception('InteractiveNotFound');
171+
throw new \Exception('InteractiveSlide not found');
165172
}
166173

174+
// Optional limiting of available resources.
175+
$this->checkPermission($interactive, $resource);
176+
167177
$feed = $slide->getFeed();
168178

169179
if (null === $feed) {
170-
throw new \Exception('Slide.feed not set.');
180+
throw new \Exception('Slide feed not set.');
171181
}
172182

173183
if (!in_array($resource, $feed->getConfiguration()['resources'] ?? [])) {
@@ -247,7 +257,7 @@ private function createEntry(string $resource, array $schedules, string $startFo
247257
*/
248258
private function quickBook(Slide $slide, InteractionSlideRequest $interactionRequest): array
249259
{
250-
$resource = $this->getValueFromInterval('resource', $interactionRequest);
260+
$resource = (string) $this->getValueFromInterval('resource', $interactionRequest);
251261
$durationMinutes = $this->getValueFromInterval('durationMinutes', $interactionRequest);
252262

253263
$now = new \DateTime();
@@ -273,25 +283,28 @@ function (CacheItemInterface $item) use ($now): \DateTime {
273283
$interactive = $this->interactiveService->getInteractiveSlide($tenant, $interactionRequest->implementationClass);
274284

275285
if (null === $interactive) {
276-
throw new \Exception('InteractiveNotFound');
286+
throw new BadRequestHttpException('Interactive not found');
277287
}
278288

289+
// Optional limiting of available resources.
290+
$this->checkPermission($interactive, $resource);
291+
279292
$feed = $slide->getFeed();
280293

281294
if (null === $feed) {
282-
throw new \Exception('Slide.feed not set.');
295+
throw new BadRequestHttpException('Slide feed not set.');
283296
}
284297

285298
if (!in_array($resource, $feed->getConfiguration()['resources'] ?? [])) {
286-
throw new \Exception('Resource not in feed resources');
299+
throw new BadRequestHttpException('Resource not in feed resources');
287300
}
288301

289302
$token = $this->getToken($tenant, $interactive);
290303

291304
$configuration = $interactive->getConfiguration();
292305

293306
if (null === $configuration) {
294-
throw new \Exception('InteractiveNoConfiguration');
307+
throw new BadRequestHttpException('Interactive no configuration');
295308
}
296309

297310
$username = $this->keyValueService->getValue($configuration['username']);
@@ -411,13 +424,13 @@ private function getValueFromInterval(string $key, InteractionSlideRequest $inte
411424
$interval = $interactionRequest->data['interval'] ?? null;
412425

413426
if (null === $interval) {
414-
throw new \Exception('interval not set.');
427+
throw new BadRequestHttpException('interval not set.');
415428
}
416429

417430
$value = $interval[$key] ?? null;
418431

419432
if (null === $value) {
420-
throw new \Exception("interval.'.$key.' not set.");
433+
throw new BadRequestHttpException("interval.'.$key.' not set.");
421434
}
422435

423436
return $value;
@@ -431,4 +444,51 @@ private function getHeaders(string $token): array
431444
'Accept' => 'application/json',
432445
];
433446
}
447+
448+
private function checkPermission(InteractiveSlide $interactive, string $resource): void
449+
{
450+
$configuration = $interactive->getConfiguration();
451+
// Optional limiting of available resources.
452+
if (null !== $configuration && !empty($configuration['resourceEndpoint'])) {
453+
$allowedResources = $this->getAllowedResources($interactive);
454+
455+
if (!in_array($resource, $allowedResources)) {
456+
throw new \Exception('Not allowed');
457+
}
458+
}
459+
}
460+
461+
private function getAllowedResources(InteractiveSlide $interactive): array
462+
{
463+
return $this->interactiveSlideCache->get(self::CACHE_ALLOWED_RESOURCES_PREFIX.$interactive->getId(), function (CacheItemInterface $item) use ($interactive) {
464+
$item->expiresAfter(60 * 60);
465+
466+
$configuration = $interactive->getConfiguration();
467+
468+
$key = $configuration['resourceEndpoint'] ?? null;
469+
470+
if (null === $key) {
471+
throw new \Exception('resourceEndpoint not set');
472+
}
473+
474+
$resourceEndpoint = $this->keyValueService->getValue($key);
475+
476+
if (null === $resourceEndpoint) {
477+
throw new \Exception('resourceEndpoint value not set');
478+
}
479+
480+
$response = $this->client->request('GET', $resourceEndpoint);
481+
$content = $response->toArray();
482+
483+
$allowedResources = [];
484+
485+
foreach ($content as $resource) {
486+
if ('True' === $resource['allowInstantBooking']) {
487+
$allowedResources[] = $resource['ResourceMail'];
488+
}
489+
}
490+
491+
return $allowedResources;
492+
});
493+
}
434494
}

0 commit comments

Comments
 (0)