authorizeEvent($request, $event); $tokens = $event->joinTokens() ->orderByDesc('created_at') ->get(); return EventJoinTokenResource::collection($tokens); } public function store(Request $request, Event $event): JsonResponse { $this->authorizeEvent($request, $event); $validated = $this->validatePayload($request); $token = $this->joinTokenService->createToken($event, array_merge($validated, [ 'created_by' => Auth::id(), ])); return (new EventJoinTokenResource($token)) ->response() ->setStatusCode(201); } public function update(Request $request, Event $event, EventJoinToken $joinToken): EventJoinTokenResource { $this->authorizeEvent($request, $event); if ($joinToken->event_id !== $event->id) { abort(404); } $validated = $this->validatePayload($request, true); $payload = []; if (array_key_exists('label', $validated)) { $payload['label'] = $validated['label']; } if (array_key_exists('expires_at', $validated)) { $payload['expires_at'] = $validated['expires_at']; } if (array_key_exists('usage_limit', $validated)) { $payload['usage_limit'] = $validated['usage_limit']; } if (! empty($payload)) { $joinToken->fill($payload); } if (array_key_exists('metadata', $validated)) { $current = is_array($joinToken->metadata) ? $joinToken->metadata : []; $incoming = $validated['metadata']; if ($incoming === null) { $joinToken->metadata = null; } else { $joinToken->metadata = array_replace_recursive($current, $incoming); } } $joinToken->save(); return new EventJoinTokenResource($joinToken->fresh()); } public function destroy(Request $request, Event $event, EventJoinToken $joinToken): EventJoinTokenResource { $this->authorizeEvent($request, $event); if ($joinToken->event_id !== $event->id) { abort(404); } $reason = $request->input('reason'); $token = $this->joinTokenService->revoke($joinToken, $reason); return new EventJoinTokenResource($token); } private function authorizeEvent(Request $request, Event $event): void { $tenantId = $request->attributes->get('tenant_id'); if ($event->tenant_id !== $tenantId) { abort(404, 'Event not found'); } } private function validatePayload(Request $request, bool $partial = false): array { $rules = [ 'label' => [$partial ? 'nullable' : 'sometimes', 'string', 'max:255'], 'expires_at' => [$partial ? 'nullable' : 'sometimes', 'nullable', 'date', 'after:now'], 'usage_limit' => [$partial ? 'nullable' : 'sometimes', 'nullable', 'integer', 'min:1'], 'metadata' => [$partial ? 'nullable' : 'sometimes', 'nullable', 'array'], 'metadata.layout_customization' => ['nullable', 'array'], 'metadata.layout_customization.layout_id' => ['nullable', 'string', 'max:100'], 'metadata.layout_customization.headline' => ['nullable', 'string', 'max:120'], 'metadata.layout_customization.subtitle' => ['nullable', 'string', 'max:160'], 'metadata.layout_customization.description' => ['nullable', 'string', 'max:500'], 'metadata.layout_customization.badge_label' => ['nullable', 'string', 'max:80'], 'metadata.layout_customization.instructions_heading' => ['nullable', 'string', 'max:120'], 'metadata.layout_customization.link_heading' => ['nullable', 'string', 'max:120'], 'metadata.layout_customization.cta_label' => ['nullable', 'string', 'max:120'], 'metadata.layout_customization.cta_caption' => ['nullable', 'string', 'max:160'], 'metadata.layout_customization.link_label' => ['nullable', 'string', 'max:160'], 'metadata.layout_customization.instructions' => ['nullable', 'array', 'max:6'], 'metadata.layout_customization.instructions.*' => ['nullable', 'string', 'max:160'], 'metadata.layout_customization.logo_url' => ['nullable', 'string', 'max:2048'], 'metadata.layout_customization.logo_data_url' => ['nullable', 'string'], 'metadata.layout_customization.accent_color' => ['nullable', 'string', 'regex:/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/'], 'metadata.layout_customization.text_color' => ['nullable', 'string', 'regex:/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/'], 'metadata.layout_customization.background_color' => ['nullable', 'string', 'regex:/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/'], 'metadata.layout_customization.secondary_color' => ['nullable', 'string', 'regex:/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/'], 'metadata.layout_customization.badge_color' => ['nullable', 'string', 'regex:/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/'], 'metadata.layout_customization.background_gradient' => ['nullable', 'array'], 'metadata.layout_customization.background_gradient.angle' => ['nullable', 'numeric'], 'metadata.layout_customization.background_gradient.stops' => ['nullable', 'array', 'max:5'], 'metadata.layout_customization.background_gradient.stops.*' => ['nullable', 'string', 'regex:/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/'], 'metadata.layout_customization.mode' => ['nullable', Rule::in(['standard', 'advanced'])], 'metadata.layout_customization.elements' => ['nullable', 'array', 'max:50'], 'metadata.layout_customization.elements.*.id' => ['required_with:metadata.layout_customization.elements', 'string', 'max:120'], 'metadata.layout_customization.elements.*.type' => ['required_with:metadata.layout_customization.elements', Rule::in(['qr', 'headline', 'subtitle', 'description', 'link', 'badge', 'logo', 'cta', 'text'])], 'metadata.layout_customization.elements.*.x' => ['nullable', 'numeric', 'min:0'], 'metadata.layout_customization.elements.*.y' => ['nullable', 'numeric', 'min:0'], 'metadata.layout_customization.elements.*.width' => ['nullable', 'numeric', 'min:40'], 'metadata.layout_customization.elements.*.height' => ['nullable', 'numeric', 'min:40'], 'metadata.layout_customization.elements.*.rotation' => ['nullable', 'numeric'], 'metadata.layout_customization.elements.*.font_size' => ['nullable', 'numeric', 'min:8', 'max:160'], 'metadata.layout_customization.elements.*.align' => ['nullable', Rule::in(['left', 'center', 'right'])], 'metadata.layout_customization.elements.*.content' => ['nullable', 'string', 'max:400'], 'metadata.layout_customization.elements.*.font_family' => ['nullable', 'string', 'max:120'], 'metadata.layout_customization.elements.*.letter_spacing' => ['nullable', 'numeric', 'min:-5', 'max:20'], 'metadata.layout_customization.elements.*.line_height' => ['nullable', 'numeric', 'min:0.5', 'max:3'], 'metadata.layout_customization.elements.*.fill' => ['nullable', 'string', 'max:20'], 'metadata.layout_customization.elements.*.locked' => ['nullable', 'boolean'], 'metadata.layout_customization.elements.*.initial' => ['nullable', 'boolean'], ]; $validated = $request->validate($rules); if (isset($validated['metadata']['layout_customization']['instructions'])) { $validated['metadata']['layout_customization']['instructions'] = array_values(array_filter( $validated['metadata']['layout_customization']['instructions'], fn ($value) => is_string($value) && trim($value) !== '' )); } if (isset($validated['metadata']['layout_customization']['logo_data_url']) && ! is_string($validated['metadata']['layout_customization']['logo_data_url'])) { unset($validated['metadata']['layout_customization']['logo_data_url']); } if (isset($validated['metadata']['layout_customization']['elements']) && is_array($validated['metadata']['layout_customization']['elements'])) { $validated['metadata']['layout_customization']['elements'] = array_values(array_filter(array_map( static function ($element) { if (! is_array($element) || empty($element['id']) || empty($element['type'])) { return null; } return array_filter([ 'id' => (string) $element['id'], 'type' => (string) $element['type'], 'x' => array_key_exists('x', $element) ? (float) $element['x'] : null, 'y' => array_key_exists('y', $element) ? (float) $element['y'] : null, 'width' => array_key_exists('width', $element) ? (float) $element['width'] : null, 'height' => array_key_exists('height', $element) ? (float) $element['height'] : null, 'rotation' => array_key_exists('rotation', $element) ? (float) $element['rotation'] : null, 'font_size' => array_key_exists('font_size', $element) ? (float) $element['font_size'] : null, 'align' => $element['align'] ?? null, 'content' => array_key_exists('content', $element) ? (string) $element['content'] : null, 'font_family' => $element['font_family'] ?? null, 'letter_spacing' => array_key_exists('letter_spacing', $element) ? (float) $element['letter_spacing'] : null, 'line_height' => array_key_exists('line_height', $element) ? (float) $element['line_height'] : null, 'fill' => $element['fill'] ?? null, 'locked' => array_key_exists('locked', $element) ? (bool) $element['locked'] : null, ], static fn ($value) => $value !== null && $value !== ''); }, $validated['metadata']['layout_customization']['elements'] ))); } return $validated; } }