validate([ 'email' => ['required', 'email'], 'password' => ['required', 'string'], ]); if (! Auth::attempt($creds)) { return ApiError::response( 'invalid_credentials', 'Invalid Credentials', 'The provided credentials are incorrect.', Response::HTTP_UNAUTHORIZED, ['email' => $creds['email']] ); } /** @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 $this->forbiddenResponse('events.show', ['event_id' => $ev->id, 'tenant_id' => $tenantId]); } 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 $this->forbiddenResponse('events.update', ['event_id' => $ev->id, 'tenant_id' => $tenantId]); } $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 $this->forbiddenResponse('events.toggle', ['event_id' => $ev->id, 'tenant_id' => $tenantId]); } $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 $this->forbiddenResponse('events.stats', ['event_id' => $ev->id, 'tenant_id' => $tenantId]); } $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(Request $request, int $id) { $u = Auth::user(); $tenantId = $u->tenant_id ?? null; $ev = Event::findOrFail($id); if ($tenantId && $ev->tenant_id !== $tenantId) { return $this->forbiddenResponse('events.invite', ['event_id' => $ev->id, 'tenant_id' => $tenantId]); } $joinToken = $this->joinTokenService->createToken($ev, [ 'created_by' => $u?->id, ]); return response()->json([ 'link' => url('/e/'.$joinToken->token), 'legacy_link' => url('/e/'.$ev->slug).'?invite='.$joinToken->token, 'token' => $joinToken->token, ]); } public function eventPhotos(int $id) { $u = Auth::user(); $tenantId = $u->tenant_id ?? null; $ev = Event::findOrFail($id); if ($tenantId && $ev->tenant_id !== $tenantId) { return $this->forbiddenResponse('events.photos', ['event_id' => $ev->id, 'tenant_id' => $tenantId]); } $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]); } private function forbiddenResponse(string $action, array $meta = []): JsonResponse { return ApiError::response( 'forbidden', 'Forbidden', 'You are not allowed to perform this action.', Response::HTTP_FORBIDDEN, array_merge(['action' => $action], $meta) ); } 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); } } }