Initialize repo and add session changes (2025-09-08)
This commit is contained in:
218
app/Http/Controllers/Api/EventPublicController.php
Normal file
218
app/Http/Controllers/Api/EventPublicController.php
Normal file
@@ -0,0 +1,218 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
207
app/Http/Controllers/Api/TenantController.php
Normal file
207
app/Http/Controllers/Api/TenantController.php
Normal file
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\Event;
|
||||
use App\Models\Photo;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class TenantController extends BaseController
|
||||
{
|
||||
public function login(Request $request)
|
||||
{
|
||||
$creds = $request->validate([
|
||||
'email' => ['required','email'],
|
||||
'password' => ['required','string'],
|
||||
]);
|
||||
if (! Auth::attempt($creds)) {
|
||||
return response()->json(['error' => ['code' => 'invalid_credentials']], 401);
|
||||
}
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
// naive token (cache-based), expires in 8 hours
|
||||
$token = Str::random(80);
|
||||
Cache::put('api_token:'.$token, $user->id, now()->addHours(8));
|
||||
return response()->json([
|
||||
'token' => $token,
|
||||
'user' => [
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'tenant_id' => $user->tenant_id ?? null,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function me(Request $request)
|
||||
{
|
||||
$u = Auth::user();
|
||||
return response()->json([
|
||||
'id' => $u->id,
|
||||
'name' => $u->name,
|
||||
'email' => $u->email,
|
||||
'tenant_id' => $u->tenant_id ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function events()
|
||||
{
|
||||
$u = Auth::user();
|
||||
$tenantId = $u->tenant_id ?? null;
|
||||
$q = Event::query();
|
||||
if ($tenantId) {
|
||||
$q->where('tenant_id', $tenantId);
|
||||
}
|
||||
return response()->json(['data' => $q->orderByDesc('created_at')->limit(100)->get(['id','name','slug','date','is_active'])]);
|
||||
}
|
||||
|
||||
public function showEvent(int $id)
|
||||
{
|
||||
$u = Auth::user();
|
||||
$tenantId = $u->tenant_id ?? null;
|
||||
$ev = Event::findOrFail($id);
|
||||
if ($tenantId && $ev->tenant_id !== $tenantId) {
|
||||
return response()->json(['error' => ['code' => 'forbidden']], 403);
|
||||
}
|
||||
return response()->json($ev->only(['id','name','slug','date','is_active','default_locale']));
|
||||
}
|
||||
|
||||
public function storeEvent(Request $request)
|
||||
{
|
||||
$u = Auth::user();
|
||||
$tenantId = $u->tenant_id ?? null;
|
||||
$data = $request->validate([
|
||||
'name' => ['required','string','max:255'],
|
||||
'slug' => ['required','string','max:255'],
|
||||
'date' => ['nullable','date'],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
$ev = new Event();
|
||||
$ev->tenant_id = $tenantId ?? $ev->tenant_id;
|
||||
$ev->name = ['de' => $data['name'], 'en' => $data['name']];
|
||||
$ev->slug = $data['slug'];
|
||||
$ev->date = $data['date'] ?? null;
|
||||
$ev->is_active = (bool)($data['is_active'] ?? true);
|
||||
$ev->default_locale = 'de';
|
||||
$ev->save();
|
||||
return response()->json(['id' => $ev->id]);
|
||||
}
|
||||
|
||||
public function updateEvent(Request $request, int $id)
|
||||
{
|
||||
$u = Auth::user();
|
||||
$tenantId = $u->tenant_id ?? null;
|
||||
$ev = Event::findOrFail($id);
|
||||
if ($tenantId && $ev->tenant_id !== $tenantId) {
|
||||
return response()->json(['error' => ['code' => 'forbidden']], 403);
|
||||
}
|
||||
$data = $request->validate([
|
||||
'name' => ['nullable','string','max:255'],
|
||||
'slug' => ['nullable','string','max:255'],
|
||||
'date' => ['nullable','date'],
|
||||
'is_active' => ['nullable','boolean'],
|
||||
]);
|
||||
if (isset($data['name'])) $ev->name = ['de' => $data['name'], 'en' => $data['name']];
|
||||
if (isset($data['slug'])) $ev->slug = $data['slug'];
|
||||
if (array_key_exists('date', $data)) $ev->date = $data['date'];
|
||||
if (array_key_exists('is_active', $data)) $ev->is_active = (bool)$data['is_active'];
|
||||
$ev->save();
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
|
||||
public function toggleEvent(int $id)
|
||||
{
|
||||
$u = Auth::user();
|
||||
$tenantId = $u->tenant_id ?? null;
|
||||
$ev = Event::findOrFail($id);
|
||||
if ($tenantId && $ev->tenant_id !== $tenantId) {
|
||||
return response()->json(['error' => ['code' => 'forbidden']], 403);
|
||||
}
|
||||
$ev->is_active = ! (bool) $ev->is_active;
|
||||
$ev->save();
|
||||
return response()->json(['is_active' => (bool)$ev->is_active]);
|
||||
}
|
||||
|
||||
public function eventStats(int $id)
|
||||
{
|
||||
$u = Auth::user();
|
||||
$tenantId = $u->tenant_id ?? null;
|
||||
$ev = Event::findOrFail($id);
|
||||
if ($tenantId && $ev->tenant_id !== $tenantId) {
|
||||
return response()->json(['error' => ['code' => 'forbidden']], 403);
|
||||
}
|
||||
$total = Photo::where('event_id', $id)->count();
|
||||
$featured = Photo::where('event_id', $id)->where('is_featured', 1)->count();
|
||||
$likes = Photo::where('event_id', $id)->sum('likes_count');
|
||||
return response()->json([
|
||||
'total' => (int)$total,
|
||||
'featured' => (int)$featured,
|
||||
'likes' => (int)$likes,
|
||||
]);
|
||||
}
|
||||
|
||||
public function createInvite(int $id)
|
||||
{
|
||||
$u = Auth::user();
|
||||
$tenantId = $u->tenant_id ?? null;
|
||||
$ev = Event::findOrFail($id);
|
||||
if ($tenantId && $ev->tenant_id !== $tenantId) {
|
||||
return response()->json(['error' => ['code' => 'forbidden']], 403);
|
||||
}
|
||||
$token = Str::random(32);
|
||||
Cache::put('invite:'.$token, $ev->slug, now()->addDays(2));
|
||||
$link = url('/e/'.$ev->slug).'?t='.$token;
|
||||
return response()->json(['link' => $link]);
|
||||
}
|
||||
|
||||
public function eventPhotos(int $id)
|
||||
{
|
||||
$u = Auth::user();
|
||||
$tenantId = $u->tenant_id ?? null;
|
||||
$ev = Event::findOrFail($id);
|
||||
if ($tenantId && $ev->tenant_id !== $tenantId) {
|
||||
return response()->json(['error' => ['code' => 'forbidden']], 403);
|
||||
}
|
||||
$rows = Photo::where('event_id', $id)->orderByDesc('created_at')->limit(100)->get(['id','thumbnail_path','file_path','likes_count','is_featured','created_at']);
|
||||
return response()->json(['data' => $rows]);
|
||||
}
|
||||
|
||||
public function featurePhoto(int $photoId)
|
||||
{
|
||||
$p = Photo::findOrFail($photoId);
|
||||
$this->authorizePhoto($p);
|
||||
$p->is_featured = 1; $p->save();
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
|
||||
public function unfeaturePhoto(int $photoId)
|
||||
{
|
||||
$p = Photo::findOrFail($photoId);
|
||||
$this->authorizePhoto($p);
|
||||
$p->is_featured = 0; $p->save();
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
|
||||
public function deletePhoto(int $photoId)
|
||||
{
|
||||
$p = Photo::findOrFail($photoId);
|
||||
$this->authorizePhoto($p);
|
||||
$p->delete();
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
|
||||
protected function authorizePhoto(Photo $p): void
|
||||
{
|
||||
$u = Auth::user();
|
||||
$tenantId = $u->tenant_id ?? null;
|
||||
$event = Event::find($p->event_id);
|
||||
if ($tenantId && $event && $event->tenant_id !== $tenantId) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user