Fix 401 errors for Guest PWA API endpoints: Remove global tenant middleware from bootstrap/app.php and apply only to tenant routes; add throttle:100,1 to guest routes in api.php; enhance EventPublicController with published status validation for all methods to ensure secure public access without auth.

This commit is contained in:
2025-09-17 20:15:08 +02:00
parent 42d6e98dff
commit c8a9224ab0
5 changed files with 2843 additions and 48 deletions

View File

@@ -47,11 +47,11 @@ class EventPublicController extends BaseController
}
public function event(string $slug)
{
$event = DB::table('events')->where('slug', $slug)->first([
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->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(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
}
$locale = request()->query('locale', 'de');
@@ -88,9 +88,9 @@ class EventPublicController extends BaseController
public function stats(string $slug)
{
$event = DB::table('events')->where('slug', $slug)->first(['id']);
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
if (! $event) {
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found']], 404);
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
}
$eventId = $event->id;
@@ -127,9 +127,9 @@ class EventPublicController extends BaseController
public function emotions(string $slug)
{
$event = DB::table('events')->where('slug', $slug)->first(['id']);
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
if (! $event) {
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found']], 404);
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
}
$rows = DB::table('emotions')
@@ -176,9 +176,9 @@ class EventPublicController extends BaseController
public function tasks(string $slug, Request $request)
{
$event = DB::table('events')->where('slug', $slug)->first(['id']);
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
if (! $event) {
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found']], 404);
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
}
$query = DB::table('tasks')
@@ -260,9 +260,9 @@ class EventPublicController extends BaseController
public function photos(Request $request, string $slug)
{
$event = DB::table('events')->where('slug', $slug)->first(['id']);
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
if (! $event) {
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found']], 404);
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
}
$eventId = $event->id;
@@ -327,6 +327,7 @@ class EventPublicController extends BaseController
public function photo(int $id)
{
$row = DB::table('photos')
->join('events', 'photos.event_id', '=', 'events.id')
->leftJoin('tasks', 'photos.task_id', '=', 'tasks.id')
->select([
'photos.id',
@@ -340,9 +341,10 @@ class EventPublicController extends BaseController
'tasks.title as task_title'
])
->where('photos.id', $id)
->where('events.status', 'published')
->first();
if (! $row) {
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Photo not found']], 404);
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Photo not found or event not public']], 404);
}
$row->file_path = $this->toPublicUrl((string)($row->file_path ?? ''));
$row->thumbnail_path = $this->toPublicUrl((string)($row->thumbnail_path ?? ''));
@@ -364,9 +366,13 @@ class EventPublicController extends BaseController
$deviceId = 'anon';
}
$photo = DB::table('photos')->where('id', $id)->first(['id', 'event_id']);
$photo = DB::table('photos')
->join('events', 'photos.event_id', '=', 'events.id')
->where('photos.id', $id)
->where('events.status', 'published')
->first(['photos.id', 'photos.event_id']);
if (! $photo) {
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Photo not found']], 404);
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Photo not found or event not public']], 404);
}
// Idempotent like per device
@@ -401,9 +407,9 @@ class EventPublicController extends BaseController
public function upload(Request $request, string $slug)
{
$event = DB::table('events')->where('slug', $slug)->first(['id']);
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
if (! $event) {
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found']], 404);
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
}
$deviceId = (string) $request->header('X-Device-Id', 'anon');

View File

@@ -25,11 +25,7 @@ return Application::configure(basePath: dirname(__DIR__))
AddLinkHeadersForPreloadedAssets::class,
]);
$middleware->api(append: [
\App\Http\Middleware\TenantTokenGuard::class,
\App\Http\Middleware\TenantIsolation::class,
\App\Http\Middleware\CreditCheckMiddleware::class,
]);
$middleware->api(append: []);
})
->withExceptions(function (Exceptions $exceptions) {
//

2823
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@
"prettier": "^3.4.2",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-tailwindcss": "^0.6.11",
"shadcn": "^3.3.1",
"typescript-eslint": "^8.23.0"
},
"dependencies": {

View File

@@ -12,19 +12,20 @@ Route::prefix('v1')->name('api.v1.')->group(function () {
Route::get('/oauth/authorize', [OAuthController::class, 'authorize'])->name('oauth.authorize');
Route::post('/oauth/token', [OAuthController::class, 'token'])->name('oauth.token');
// Guest PWA routes (public, no auth)
Route::get('/events/{slug}', [EventPublicController::class, 'event'])->name('events.show');
Route::get('/events/{slug}/stats', [EventPublicController::class, 'stats'])->name('events.stats');
Route::get('/events/{slug}/emotions', [EventPublicController::class, 'emotions'])->name('events.emotions');
Route::get('/events/{slug}/tasks', [EventPublicController::class, 'tasks'])->name('events.tasks');
Route::get('/events/{slug}/photos', [EventPublicController::class, 'photos'])->name('events.photos');
Route::get('/photos/{id}', [EventPublicController::class, 'photo'])->name('photos.show');
Route::post('/photos/{id}/like', [EventPublicController::class, 'like'])->name('photos.like');
Route::post('/events/{slug}/upload', [EventPublicController::class, 'upload'])->name('events.upload');
// Guest PWA routes (public, throttled, no tenant middleware)
Route::middleware('throttle:100,1')->group(function () {
Route::get('/events/{slug}', [EventPublicController::class, 'event'])->name('events.show');
Route::get('/events/{slug}/stats', [EventPublicController::class, 'stats'])->name('events.stats');
Route::get('/events/{slug}/emotions', [EventPublicController::class, 'emotions'])->name('events.emotions');
Route::get('/events/{slug}/tasks', [EventPublicController::class, 'tasks'])->name('events.tasks');
Route::get('/events/{slug}/photos', [EventPublicController::class, 'photos'])->name('events.photos');
Route::get('/photos/{id}', [EventPublicController::class, 'photo'])->name('photos.show');
Route::post('/photos/{id}/like', [EventPublicController::class, 'like'])->name('photos.like');
Route::post('/events/{slug}/upload', [EventPublicController::class, 'upload'])->name('events.upload');
});
// Protected tenant API routes (require auth:sanctum)
Route::middleware('auth:sanctum')->prefix('tenant')->group(function () {
// Protected tenant API routes (require auth:sanctum + tenant middleware)
Route::middleware(['auth:sanctum', \App\Http\Middleware\TenantTokenGuard::class, \App\Http\Middleware\TenantIsolation::class])->prefix('tenant')->group(function () {
Route::get('me', [OAuthController::class, 'me'])->name('tenant.me');
// Events CRUD