Adjust join token expiry for event dates
This commit is contained in:
@@ -882,9 +882,16 @@ class EventController extends Controller
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$minimumExpiry = $this->joinTokenService->minimumExpiryForEvent($event);
|
||||||
|
$expiresAtRules = ['nullable', 'date', 'after:now'];
|
||||||
|
|
||||||
|
if ($minimumExpiry) {
|
||||||
|
$expiresAtRules[] = 'after_or_equal:'.$minimumExpiry->toDateTimeString();
|
||||||
|
}
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'label' => ['nullable', 'string', 'max:255'],
|
'label' => ['nullable', 'string', 'max:255'],
|
||||||
'expires_at' => ['nullable', 'date', 'after:now'],
|
'expires_at' => $expiresAtRules,
|
||||||
'usage_limit' => ['nullable', 'integer', 'min:1'],
|
'usage_limit' => ['nullable', 'integer', 'min:1'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class EventJoinTokenController extends Controller
|
|||||||
{
|
{
|
||||||
$this->authorizeEvent($request, $event, 'join-tokens:manage');
|
$this->authorizeEvent($request, $event, 'join-tokens:manage');
|
||||||
|
|
||||||
$validated = $this->validatePayload($request);
|
$validated = $this->validatePayload($request, $event);
|
||||||
|
|
||||||
$token = $this->joinTokenService->createToken($event, array_merge($validated, [
|
$token = $this->joinTokenService->createToken($event, array_merge($validated, [
|
||||||
'created_by' => Auth::id(),
|
'created_by' => Auth::id(),
|
||||||
@@ -52,7 +52,7 @@ class EventJoinTokenController extends Controller
|
|||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
$validated = $this->validatePayload($request, true);
|
$validated = $this->validatePayload($request, $event, true);
|
||||||
|
|
||||||
$payload = [];
|
$payload = [];
|
||||||
|
|
||||||
@@ -115,11 +115,18 @@ class EventJoinTokenController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validatePayload(Request $request, bool $partial = false): array
|
private function validatePayload(Request $request, Event $event, bool $partial = false): array
|
||||||
{
|
{
|
||||||
|
$minimumExpiry = $this->joinTokenService->minimumExpiryForEvent($event);
|
||||||
|
$expiresAtRules = [$partial ? 'nullable' : 'sometimes', 'nullable', 'date', 'after:now'];
|
||||||
|
|
||||||
|
if ($minimumExpiry) {
|
||||||
|
$expiresAtRules[] = 'after_or_equal:'.$minimumExpiry->toDateTimeString();
|
||||||
|
}
|
||||||
|
|
||||||
$rules = [
|
$rules = [
|
||||||
'label' => [$partial ? 'nullable' : 'sometimes', 'string', 'max:255'],
|
'label' => [$partial ? 'nullable' : 'sometimes', 'string', 'max:255'],
|
||||||
'expires_at' => [$partial ? 'nullable' : 'sometimes', 'nullable', 'date', 'after:now'],
|
'expires_at' => $expiresAtRules,
|
||||||
'usage_limit' => [$partial ? 'nullable' : 'sometimes', 'nullable', 'integer', 'min:1'],
|
'usage_limit' => [$partial ? 'nullable' : 'sometimes', 'nullable', 'integer', 'min:1'],
|
||||||
'metadata' => [$partial ? 'nullable' : 'sometimes', 'nullable', 'array'],
|
'metadata' => [$partial ? 'nullable' : 'sometimes', 'nullable', 'array'],
|
||||||
'metadata.layout_customization' => ['nullable', 'array'],
|
'metadata.layout_customization' => ['nullable', 'array'],
|
||||||
|
|||||||
@@ -40,6 +40,14 @@ class Event extends Model
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static::updated(function (self $event): void {
|
||||||
|
if (! $event->wasChanged('date')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app(EventJoinTokenService::class)->extendExpiryForEvent($event);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function storageAssignments(): HasMany
|
public function storageAssignments(): HasMany
|
||||||
|
|||||||
@@ -27,14 +27,21 @@ class EventJoinTokenService
|
|||||||
];
|
];
|
||||||
|
|
||||||
if ($expiresAt = Arr::get($attributes, 'expires_at')) {
|
if ($expiresAt = Arr::get($attributes, 'expires_at')) {
|
||||||
$payload['expires_at'] = $expiresAt instanceof Carbon
|
$resolvedExpiry = $expiresAt instanceof Carbon
|
||||||
? $expiresAt
|
? $expiresAt
|
||||||
: Carbon::parse($expiresAt);
|
: Carbon::parse($expiresAt);
|
||||||
} else {
|
|
||||||
$ttlHours = (int) (GuestPolicySetting::current()->join_token_ttl_hours ?? 0);
|
|
||||||
|
|
||||||
if ($ttlHours > 0) {
|
$minimumExpiry = $this->minimumExpiryForEvent($event);
|
||||||
$payload['expires_at'] = now()->addHours($ttlHours);
|
if ($minimumExpiry && $resolvedExpiry->lessThan($minimumExpiry)) {
|
||||||
|
$resolvedExpiry = $minimumExpiry;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload['expires_at'] = $resolvedExpiry;
|
||||||
|
} else {
|
||||||
|
$defaultExpiry = $this->defaultExpiryForEvent($event);
|
||||||
|
|
||||||
|
if ($defaultExpiry) {
|
||||||
|
$payload['expires_at'] = $defaultExpiry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +55,21 @@ class EventJoinTokenService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function extendExpiryForEvent(Event $event): int
|
||||||
|
{
|
||||||
|
$minimumExpiry = $this->minimumExpiryForEvent($event);
|
||||||
|
|
||||||
|
if (! $minimumExpiry) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $event->joinTokens()
|
||||||
|
->whereNull('revoked_at')
|
||||||
|
->whereNotNull('expires_at')
|
||||||
|
->where('expires_at', '<', $minimumExpiry)
|
||||||
|
->update(['expires_at' => $minimumExpiry]);
|
||||||
|
}
|
||||||
|
|
||||||
public function revoke(EventJoinToken $joinToken, ?string $reason = null): EventJoinToken
|
public function revoke(EventJoinToken $joinToken, ?string $reason = null): EventJoinToken
|
||||||
{
|
{
|
||||||
unset($joinToken->plain_token);
|
unset($joinToken->plain_token);
|
||||||
@@ -162,6 +184,61 @@ class EventJoinTokenService
|
|||||||
return hash('sha256', $token);
|
return hash('sha256', $token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function minimumExpiryForEvent(Event $event): ?Carbon
|
||||||
|
{
|
||||||
|
$eventDate = $this->resolveEventDate($event);
|
||||||
|
|
||||||
|
if (! $eventDate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ttlHours = $this->defaultTtlHours();
|
||||||
|
|
||||||
|
if ($ttlHours > 0) {
|
||||||
|
return $eventDate->copy()->addHours($ttlHours);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $eventDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function defaultExpiryForEvent(Event $event): ?Carbon
|
||||||
|
{
|
||||||
|
$ttlHours = $this->defaultTtlHours();
|
||||||
|
|
||||||
|
if ($ttlHours <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultExpiry = now()->addHours($ttlHours);
|
||||||
|
$minimumExpiry = $this->minimumExpiryForEvent($event);
|
||||||
|
|
||||||
|
if ($minimumExpiry && $minimumExpiry->greaterThan($defaultExpiry)) {
|
||||||
|
$defaultExpiry = $minimumExpiry;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $defaultExpiry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveEventDate(Event $event): ?Carbon
|
||||||
|
{
|
||||||
|
if (! ($event->date instanceof Carbon)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$eventDate = $event->date->copy();
|
||||||
|
|
||||||
|
if ($eventDate->format('H:i:s') === '00:00:00') {
|
||||||
|
$eventDate = $eventDate->endOfDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $eventDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function defaultTtlHours(): int
|
||||||
|
{
|
||||||
|
return (int) (GuestPolicySetting::current()->join_token_ttl_hours ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
private function normalizeDeviceId(?string $deviceId): ?string
|
private function normalizeDeviceId(?string $deviceId): ?string
|
||||||
{
|
{
|
||||||
if (! is_string($deviceId) || trim($deviceId) === '') {
|
if (! is_string($deviceId) || trim($deviceId) === '') {
|
||||||
|
|||||||
@@ -11,21 +11,90 @@ class EventJoinTokenTtlPolicyTest extends TenantTestCase
|
|||||||
{
|
{
|
||||||
Carbon::setTestNow(Carbon::parse('2026-01-05 12:00:00'));
|
Carbon::setTestNow(Carbon::parse('2026-01-05 12:00:00'));
|
||||||
|
|
||||||
|
$eventDate = Carbon::parse('2026-06-15');
|
||||||
|
|
||||||
$event = Event::factory()
|
$event = Event::factory()
|
||||||
->for($this->tenant)
|
->for($this->tenant)
|
||||||
->create([
|
->create([
|
||||||
'name' => ['de' => 'Token TTL Test', 'en' => 'Token TTL Test'],
|
'name' => ['de' => 'Token TTL Test', 'en' => 'Token TTL Test'],
|
||||||
'slug' => 'token-ttl-test',
|
'slug' => 'token-ttl-test',
|
||||||
|
'date' => $eventDate,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->authenticatedRequest('POST', "/api/v1/tenant/events/{$event->slug}/join-tokens");
|
$response = $this->authenticatedRequest('POST', "/api/v1/tenant/events/{$event->slug}/join-tokens");
|
||||||
|
|
||||||
$response->assertCreated();
|
$response->assertCreated();
|
||||||
|
|
||||||
$expectedExpiry = now()->addDays(7)->toIso8601String();
|
$expectedExpiry = $eventDate->copy()
|
||||||
|
->endOfDay()
|
||||||
|
->addDays(7)
|
||||||
|
->toIso8601String();
|
||||||
|
|
||||||
$this->assertSame($expectedExpiry, $response->json('data.expires_at'));
|
$this->assertSame($expectedExpiry, $response->json('data.expires_at'));
|
||||||
|
|
||||||
Carbon::setTestNow();
|
Carbon::setTestNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_join_tokens_extend_when_event_date_moves_forward(): void
|
||||||
|
{
|
||||||
|
Carbon::setTestNow(Carbon::parse('2026-01-05 12:00:00'));
|
||||||
|
|
||||||
|
$event = Event::factory()
|
||||||
|
->for($this->tenant)
|
||||||
|
->create([
|
||||||
|
'name' => ['de' => 'Token Update Test', 'en' => 'Token Update Test'],
|
||||||
|
'slug' => 'token-update-test',
|
||||||
|
'date' => Carbon::parse('2026-01-10'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$token = $event->joinTokens()->firstOrFail();
|
||||||
|
|
||||||
|
$newDate = Carbon::parse('2026-04-20');
|
||||||
|
$response = $this->authenticatedRequest('PUT', "/api/v1/tenant/events/{$event->slug}", [
|
||||||
|
'event_date' => $newDate->toDateString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
|
||||||
|
$token->refresh();
|
||||||
|
|
||||||
|
$expectedExpiry = $newDate->copy()
|
||||||
|
->endOfDay()
|
||||||
|
->addDays(7);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
$expectedExpiry->toIso8601String(),
|
||||||
|
$token->expires_at?->toIso8601String()
|
||||||
|
);
|
||||||
|
|
||||||
|
Carbon::setTestNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_join_token_expiry_must_not_be_before_event_minimum(): void
|
||||||
|
{
|
||||||
|
Carbon::setTestNow(Carbon::parse('2026-01-05 12:00:00'));
|
||||||
|
|
||||||
|
$eventDate = Carbon::parse('2026-06-15');
|
||||||
|
|
||||||
|
$event = Event::factory()
|
||||||
|
->for($this->tenant)
|
||||||
|
->create([
|
||||||
|
'name' => ['de' => 'Token Expiry Guard', 'en' => 'Token Expiry Guard'],
|
||||||
|
'slug' => 'token-expiry-guard',
|
||||||
|
'date' => $eventDate,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$invalidExpiry = $eventDate->copy()
|
||||||
|
->endOfDay()
|
||||||
|
->addDay();
|
||||||
|
|
||||||
|
$response = $this->authenticatedRequest('POST', "/api/v1/tenant/events/{$event->slug}/join-tokens", [
|
||||||
|
'expires_at' => $invalidExpiry->toDateTimeString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertUnprocessable();
|
||||||
|
$response->assertInvalid(['expires_at']);
|
||||||
|
|
||||||
|
Carbon::setTestNow();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user