Added opaque join-token support across backend and frontend: new migration/model/service/endpoints, guest controllers now resolve tokens, and the demo seeder seeds a token. Tenant event details list/manage tokens with copy/revoke actions, and the guest PWA uses tokens end-to-end (routing, storage, uploads, achievements, etc.). Docs TODO updated to reflect completed steps.
This commit is contained in:
@@ -10,9 +10,45 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Support\ImageHelper;
|
||||
use App\Services\EventJoinTokenService;
|
||||
|
||||
class EventPublicController extends BaseController
|
||||
{
|
||||
public function __construct(private readonly EventJoinTokenService $joinTokenService)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: object|null, 1: \App\Models\EventJoinToken|null}
|
||||
*/
|
||||
private function resolvePublishedEvent(string $identifier, array $columns = ['id']): array
|
||||
{
|
||||
$event = DB::table('events')
|
||||
->where('slug', $identifier)
|
||||
->where('status', 'published')
|
||||
->first($columns);
|
||||
|
||||
if ($event) {
|
||||
return [$event, null];
|
||||
}
|
||||
|
||||
$joinToken = $this->joinTokenService->findActiveToken($identifier);
|
||||
if (! $joinToken) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
$event = DB::table('events')
|
||||
->where('id', $joinToken->event_id)
|
||||
->where('status', 'published')
|
||||
->first($columns);
|
||||
|
||||
if (! $event) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
return [$event, $joinToken];
|
||||
}
|
||||
|
||||
private function getLocalized($value, $locale, $default = '') {
|
||||
if (is_string($value) && json_decode($value) !== null) {
|
||||
$data = json_decode($value, true);
|
||||
@@ -45,36 +81,46 @@ class EventPublicController extends BaseController
|
||||
|
||||
return $path; // fallback as-is
|
||||
}
|
||||
public function event(string $slug)
|
||||
public function event(string $identifier)
|
||||
{
|
||||
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first([
|
||||
'id', 'slug', 'name', 'default_locale', 'created_at', 'updated_at'
|
||||
[$event, $joinToken] = $this->resolvePublishedEvent($identifier, [
|
||||
'id',
|
||||
'slug',
|
||||
'name',
|
||||
'default_locale',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'event_type_id',
|
||||
]);
|
||||
if (! $event) {
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
||||
}
|
||||
|
||||
$locale = request()->query('locale', 'de');
|
||||
$locale = request()->query('locale', $event->default_locale ?? 'de');
|
||||
$nameData = json_decode($event->name, true);
|
||||
$localizedName = $nameData[$locale] ?? $nameData['de'] ?? $event->name;
|
||||
|
||||
// Get event type for icon
|
||||
$eventType = DB::table('events')
|
||||
->join('event_types', 'events.event_type_id', '=', 'event_types.id')
|
||||
->where('events.id', $event->id)
|
||||
->first(['event_types.slug as type_slug', 'event_types.name as type_name']);
|
||||
$eventType = null;
|
||||
if ($event->event_type_id) {
|
||||
$eventType = DB::table('event_types')
|
||||
->where('id', $event->event_type_id)
|
||||
->first(['slug as type_slug', 'name as type_name']);
|
||||
}
|
||||
|
||||
$locale = request()->query('locale', 'de');
|
||||
$eventTypeData = $eventType ? [
|
||||
'slug' => $eventType->type_slug,
|
||||
'name' => $this->getLocalized($eventType->type_name, $locale, 'Event'),
|
||||
'icon' => $eventType->type_slug === 'wedding' ? '❤️' : '👥'
|
||||
'icon' => $eventType->type_slug === 'wedding' ? 'heart' : 'guests',
|
||||
] : [
|
||||
'slug' => 'general',
|
||||
'name' => $this->getLocalized('Event', $locale, 'Event'),
|
||||
'icon' => '👥'
|
||||
'icon' => 'guests',
|
||||
];
|
||||
|
||||
if ($joinToken) {
|
||||
$this->joinTokenService->incrementUsage($joinToken);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'id' => $event->id,
|
||||
'slug' => $event->slug,
|
||||
@@ -83,12 +129,13 @@ class EventPublicController extends BaseController
|
||||
'created_at' => $event->created_at,
|
||||
'updated_at' => $event->updated_at,
|
||||
'type' => $eventTypeData,
|
||||
'join_token' => $joinToken?->token,
|
||||
])->header('Cache-Control', 'no-store');
|
||||
}
|
||||
|
||||
public function stats(string $slug)
|
||||
public function stats(string $identifier)
|
||||
{
|
||||
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
|
||||
[$event] = $this->resolvePublishedEvent($identifier, ['id']);
|
||||
if (! $event) {
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
||||
}
|
||||
@@ -125,18 +172,20 @@ class EventPublicController extends BaseController
|
||||
->header('ETag', $etag);
|
||||
}
|
||||
|
||||
public function emotions(string $slug)
|
||||
public function emotions(string $identifier)
|
||||
{
|
||||
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
|
||||
[$event] = $this->resolvePublishedEvent($identifier, ['id']);
|
||||
if (! $event) {
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
||||
}
|
||||
|
||||
$eventId = $event->id;
|
||||
|
||||
$rows = DB::table('emotions')
|
||||
->join('emotion_event_type', 'emotions.id', '=', 'emotion_event_type.emotion_id')
|
||||
->join('event_types', 'emotion_event_type.event_type_id', '=', 'event_types.id')
|
||||
->join('events', 'events.event_type_id', '=', 'event_types.id')
|
||||
->where('events.id', $event->id)
|
||||
->where('events.id', $eventId)
|
||||
->select([
|
||||
'emotions.id',
|
||||
'emotions.name',
|
||||
@@ -174,17 +223,19 @@ class EventPublicController extends BaseController
|
||||
->header('ETag', $etag);
|
||||
}
|
||||
|
||||
public function tasks(string $slug, Request $request)
|
||||
public function tasks(string $identifier, Request $request)
|
||||
{
|
||||
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
|
||||
[$event] = $this->resolvePublishedEvent($identifier, ['id']);
|
||||
if (! $event) {
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
||||
}
|
||||
|
||||
$eventId = $event->id;
|
||||
|
||||
$query = DB::table('tasks')
|
||||
->join('task_collection_task', 'tasks.id', '=', 'task_collection_task.task_id')
|
||||
->join('event_task_collection', 'task_collection_task.task_collection_id', '=', 'event_task_collection.task_collection_id')
|
||||
->where('event_task_collection.event_id', $event->id)
|
||||
->where('event_task_collection.event_id', $eventId)
|
||||
->select([
|
||||
'tasks.id',
|
||||
'tasks.title',
|
||||
@@ -258,9 +309,9 @@ class EventPublicController extends BaseController
|
||||
->header('ETag', $etag);
|
||||
}
|
||||
|
||||
public function photos(Request $request, string $slug)
|
||||
public function photos(Request $request, string $identifier)
|
||||
{
|
||||
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
|
||||
[$event] = $this->resolvePublishedEvent($identifier, ['id']);
|
||||
if (! $event) {
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
||||
}
|
||||
@@ -404,18 +455,19 @@ class EventPublicController extends BaseController
|
||||
return response()->json(['liked' => true, 'likes_count' => $count]);
|
||||
}
|
||||
|
||||
public function upload(Request $request, string $slug)
|
||||
public function upload(Request $request, string $identifier)
|
||||
{
|
||||
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
|
||||
[$event] = $this->resolvePublishedEvent($identifier, ['id']);
|
||||
if (! $event) {
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
||||
}
|
||||
$eventId = $event->id;
|
||||
|
||||
$deviceId = (string) $request->header('X-Device-Id', 'anon');
|
||||
$deviceId = substr(preg_replace('/[^a-zA-Z0-9_-]/', '', $deviceId), 0, 64) ?: 'anon';
|
||||
|
||||
// Per-device cap per event (MVP: 50)
|
||||
$deviceCount = DB::table('photos')->where('event_id', $event->id)->where('guest_name', $deviceId)->count();
|
||||
$deviceCount = DB::table('photos')->where('event_id', $eventId)->where('guest_name', $deviceId)->count();
|
||||
if ($deviceCount >= 50) {
|
||||
return response()->json(['error' => ['code' => 'limit_reached', 'message' => 'Upload-Limit erreicht']], 429);
|
||||
}
|
||||
@@ -429,17 +481,17 @@ class EventPublicController extends BaseController
|
||||
]);
|
||||
|
||||
$file = $validated['photo'];
|
||||
$path = Storage::disk('public')->putFile("events/{$event->id}/photos", $file);
|
||||
$path = Storage::disk('public')->putFile("events/{$eventId}/photos", $file);
|
||||
$url = Storage::url($path);
|
||||
|
||||
// Generate thumbnail (JPEG) under photos/thumbs
|
||||
$baseName = pathinfo($path, PATHINFO_FILENAME);
|
||||
$thumbRel = "events/{$event->id}/photos/thumbs/{$baseName}_thumb.jpg";
|
||||
$thumbRel = "events/{$eventId}/photos/thumbs/{$baseName}_thumb.jpg";
|
||||
$thumbPath = ImageHelper::makeThumbnailOnDisk('public', $path, $thumbRel, 640, 82);
|
||||
$thumbUrl = $thumbPath ? Storage::url($thumbPath) : $url;
|
||||
|
||||
$id = DB::table('photos')->insertGetId([
|
||||
'event_id' => $event->id,
|
||||
'event_id' => $eventId,
|
||||
'task_id' => $validated['task_id'] ?? null,
|
||||
'guest_name' => $validated['guest_name'] ?? $deviceId,
|
||||
'file_path' => $url,
|
||||
@@ -447,7 +499,7 @@ class EventPublicController extends BaseController
|
||||
'likes_count' => 0,
|
||||
|
||||
// Handle emotion_id: prefer explicit ID, fallback to slug lookup, then default
|
||||
'emotion_id' => $this->resolveEmotionId($validated, $event->id),
|
||||
'emotion_id' => $this->resolveEmotionId($validated, $eventId),
|
||||
'is_featured' => 0,
|
||||
'metadata' => null,
|
||||
'created_at' => now(),
|
||||
@@ -502,9 +554,9 @@ class EventPublicController extends BaseController
|
||||
|
||||
return $defaultEmotion ?: 1; // Ultimate fallback to emotion ID 1 (assuming "Happy" exists)
|
||||
}
|
||||
public function achievements(Request $request, string $slug)
|
||||
public function achievements(Request $request, string $identifier)
|
||||
{
|
||||
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
|
||||
[$event] = $this->resolvePublishedEvent($identifier, ['id']);
|
||||
if (! $event) {
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
||||
}
|
||||
@@ -718,4 +770,3 @@ class EventPublicController extends BaseController
|
||||
->header('ETag', $etag);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user