Files
fotospiel-app/app/Http/Controllers/Api/EventPublicController.php
2025-09-08 14:03:43 +02:00

219 lines
8.1 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use Carbon\CarbonImmutable;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use App\Support\ImageHelper;
class EventPublicController extends BaseController
{
public function event(string $slug)
{
$event = DB::table('events')->where('slug', $slug)->first([
'id', 'slug', 'name', 'default_locale', 'created_at', 'updated_at'
]);
if (! $event) {
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found']], 404);
}
return response()->json([
'id' => $event->id,
'slug' => $event->slug,
'name' => $event->name,
'default_locale' => $event->default_locale,
'created_at' => $event->created_at,
'updated_at' => $event->updated_at,
])->header('Cache-Control', 'no-store');
}
public function stats(string $slug)
{
$event = DB::table('events')->where('slug', $slug)->first(['id']);
if (! $event) {
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found']], 404);
}
$eventId = $event->id;
// Approximate online guests as distinct recent uploaders in last 10 minutes.
$tenMinutesAgo = CarbonImmutable::now()->subMinutes(10);
$onlineGuests = DB::table('photos')
->where('event_id', $eventId)
->where('created_at', '>=', $tenMinutesAgo)
->distinct('guest_name')
->count('guest_name');
// Tasks solved as number of photos linked to a task (proxy metric).
$tasksSolved = DB::table('photos')->where('event_id', $eventId)->whereNotNull('task_id')->count();
$latestPhotoAt = DB::table('photos')->where('event_id', $eventId)->max('created_at');
$payload = [
'online_guests' => $onlineGuests,
'tasks_solved' => $tasksSolved,
'latest_photo_at' => $latestPhotoAt,
];
$etag = sha1(json_encode($payload));
$reqEtag = request()->headers->get('If-None-Match');
if ($reqEtag && $reqEtag === $etag) {
return response('', 304);
}
return response()->json($payload)
->header('Cache-Control', 'no-store')
->header('ETag', $etag);
}
public function photos(Request $request, string $slug)
{
$event = DB::table('events')->where('slug', $slug)->first(['id']);
if (! $event) {
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found']], 404);
}
$eventId = $event->id;
$since = $request->query('since');
$query = DB::table('photos')
->select(['id', 'file_path', 'thumbnail_path', 'likes_count', 'emotion_id', 'task_id', 'created_at'])
->where('event_id', $eventId)
->orderByDesc('created_at')
->limit(60);
if ($since) {
$query->where('created_at', '>', $since);
}
$rows = $query->get();
$latestPhotoAt = DB::table('photos')->where('event_id', $eventId)->max('created_at');
$payload = [
'data' => $rows,
'latest_photo_at' => $latestPhotoAt,
];
$etag = sha1(json_encode([$since, $latestPhotoAt]));
$reqEtag = request()->headers->get('If-None-Match');
if ($reqEtag && $reqEtag === $etag) {
return response('', 304);
}
return response()->json($payload)
->header('Cache-Control', 'no-store')
->header('ETag', $etag)
->header('Last-Modified', (string)$latestPhotoAt);
}
public function photo(int $id)
{
$row = DB::table('photos')
->select(['id', 'event_id', 'file_path', 'thumbnail_path', 'likes_count', 'emotion_id', 'task_id', 'created_at'])
->where('id', $id)
->first();
if (! $row) {
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Photo not found']], 404);
}
return response()->json($row)->header('Cache-Control', 'no-store');
}
public function like(Request $request, int $id)
{
$deviceId = (string) $request->header('X-Device-Id', 'anon');
$deviceId = substr(preg_replace('/[^a-zA-Z0-9_-]/', '', $deviceId), 0, 64);
if ($deviceId === '') {
$deviceId = 'anon';
}
$photo = DB::table('photos')->where('id', $id)->first(['id', 'event_id']);
if (! $photo) {
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Photo not found']], 404);
}
// Idempotent like per device
$exists = DB::table('photo_likes')->where('photo_id', $id)->where('guest_name', $deviceId)->exists();
if ($exists) {
$count = (int) DB::table('photos')->where('id', $id)->value('likes_count');
return response()->json(['liked' => true, 'likes_count' => $count]);
}
DB::beginTransaction();
try {
DB::table('photo_likes')->insert([
'photo_id' => $id,
'guest_name' => $deviceId,
'ip_address' => 'device',
'created_at' => now(),
]);
DB::table('photos')->where('id', $id)->update([
'likes_count' => DB::raw('likes_count + 1'),
'updated_at' => now(),
]);
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
Log::warning('like failed', ['error' => $e->getMessage()]);
}
$count = (int) DB::table('photos')->where('id', $id)->value('likes_count');
return response()->json(['liked' => true, 'likes_count' => $count]);
}
public function upload(Request $request, string $slug)
{
$event = DB::table('events')->where('slug', $slug)->first(['id']);
if (! $event) {
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found']], 404);
}
$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();
if ($deviceCount >= 50) {
return response()->json(['error' => ['code' => 'limit_reached', 'message' => 'Upload-Limit erreicht']], 429);
}
$validated = $request->validate([
'photo' => ['required', 'image', 'max:6144'], // 6 MB
'emotion_id' => ['nullable', 'integer'],
'task_id' => ['nullable', 'integer'],
'guest_name' => ['nullable', 'string', 'max:255'],
]);
$file = $validated['photo'];
$path = Storage::disk('public')->putFile("events/{$event->id}/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";
$thumbPath = ImageHelper::makeThumbnailOnDisk('public', $path, $thumbRel, 640, 82);
$thumbUrl = $thumbPath ? Storage::url($thumbPath) : $url;
$id = DB::table('photos')->insertGetId([
'event_id' => $event->id,
'emotion_id' => $validated['emotion_id'] ?? null,
'task_id' => $validated['task_id'] ?? null,
'guest_name' => $validated['guest_name'] ?? $deviceId,
'file_path' => $url,
'thumbnail_path' => $thumbUrl,
'likes_count' => 0,
'is_featured' => 0,
'metadata' => null,
'created_at' => now(),
'updated_at' => now(),
]);
return response()->json([
'id' => $id,
'file_path' => $url,
'thumbnail_path' => $thumbUrl,
], 201);
}
}